diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index f46693a4e80..06fcb5beece 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -119,32 +119,29 @@ APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t } // Handles partial writes, errors, and overflow buffering. -// Called when the inline fast path in the header couldn't complete the write, +// Called when the inline fast path couldn't complete the write, // or directly from cold paths (handshake, error handling). -// sent == -1 means either the fast path write returned -1, or there was overflow backlog. APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) { -#ifdef HELPER_LOG_PACKETS - for (int i = 0; i < iovcnt; i++) { - LOG_PACKET_SENDING(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); - } -#endif - - if (sent == -1) { - // Either the fast path got -1, or we were called with overflow backlog - if (!this->overflow_buf_.empty()) { - // Drain existing backlog first - APIError err = this->drain_overflow_and_handle_errors_(); - if (err != APIError::OK) - return err; - // Try again after drain + if (sent <= 0) { + if (sent == WRITE_NOT_ATTEMPTED) { + // Cold path: no write attempted yet, drain overflow and try + if (!this->overflow_buf_.empty()) { + APIError err = this->drain_overflow_and_handle_errors_(); + if (err != APIError::OK) + return err; + } if (this->overflow_buf_.empty()) { - sent = - (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); + sent = this->write_iov_to_socket_(iov, iovcnt); if (sent == static_cast(total_write_len)) return APIError::OK; + // Partial write or -1: fall through to error check / enqueue below + } else { + // Overflow backlog remains after drain; skip socket write, enqueue everything + sent = 0; } } - if (sent == -1) { + // WRITE_FAILED (-1): fast path or retry write returned -1, check errno + if (sent == WRITE_FAILED) { int err = errno; if (err != EWOULDBLOCK && err != EAGAIN) { this->state_ = State::FAILED; @@ -155,6 +152,10 @@ 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_) + if (sent == static_cast(total_write_len)) + return APIError::OK; + // Queue unsent data into overflow buffer if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast(sent))) { HELPER_LOG("Overflow buffer full, dropping connection"); diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 94007eeb3d7..9c0f072e3b9 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -192,32 +192,45 @@ class APIFrameHelper { // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors. APIError drain_overflow_and_handle_errors_(); + // Sentinel values for the sent parameter in write_raw_ methods + static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1 + static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet + + // Dispatch to write() or writev() based on iovec count + inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) { + return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); + } + // Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages) // These inline the fast path (overflow empty + full write) and tail-call the out-of-line // slow path only on failure/partial write. inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) { - ssize_t sent = -1; if (this->overflow_buf_.empty()) [[likely]] { - sent = this->socket_->write(data, len); + ssize_t sent = this->socket_->write(data, len); if (sent == static_cast(len)) [[likely]] 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, 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) { - ssize_t sent = -1; if (this->overflow_buf_.empty()) [[likely]] { - sent = this->socket_->writev(iov, iovcnt); + 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, 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 - APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = -1); - APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent = -1); + // 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); // 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 0c0c069b534..62073bb4401 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -497,14 +497,15 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff if (frame_footer_size_) buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - MessageInfo msg{type, 0, - static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + 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); if (aerr != APIError::OK) return aerr; - return this->write_raw_fast_buf_(iov.iov_base, static_cast(iov.iov_len)); + // 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)); } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { @@ -527,6 +528,11 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s total_write_len += iov.iov_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); } diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index e56006d955a..53c18431d61 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,14 +9,16 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: + // Noise header structure: + // Pos 0: indicator (0x01) + // Pos 1-2: encrypted payload size (16-bit big-endian) + // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) + // Pos 7+: actual payload data + static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx) : APIFrameHelper(std::move(socket)), ctx_(ctx) { - // Noise header structure: - // Pos 0: indicator (0x01) - // Pos 1-2: encrypted payload size (16-bit big-endian) - // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) - // Pos 7+: actual payload data - frame_header_padding_ = 7; + frame_header_padding_ = HEADER_PADDING; } ~APINoiseFrameHelper() override; APIError init() override; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 6be20f89f37..06b8dd6e7b5 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -235,9 +235,8 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } // Write plaintext header into pre-allocated padding before payload. -// Returns pointer to start of frame (header + payload are contiguous). -ESPHOME_ALWAYS_INLINE static inline uint8_t *write_plaintext_header(uint8_t *buf_start, const MessageInfo &msg, - uint8_t frame_header_padding) { +// 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 @@ -269,8 +268,8 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t *write_plaintext_header(uint8_t *buf // [6...] - Actual payload data // // The message starts at offset + frame_header_padding - // So we write the header starting at offset + frame_header_padding - total_header_len - uint32_t header_offset = frame_header_padding - total_header_len; + // So we write the header starting at offset + HEADER_PADDING - total_header_len + uint32_t header_offset = APIPlaintextFrameHelper::HEADER_PADDING - total_header_len; // Write the plaintext header buf_start[header_offset] = 0x00; // indicator @@ -279,7 +278,7 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t *write_plaintext_header(uint8_t *buf 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); - return buf_start + header_offset; + return total_header_len; } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { @@ -287,11 +286,12 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite assert(this->state_ == State::DATA); #endif - MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; + MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - HEADER_PADDING)}; uint8_t *buffer_data = buffer.get_buffer()->data(); - uint8_t *msg_start = write_plaintext_header(buffer_data, msg, frame_header_padding_); - uint8_t msg_header_len = static_cast(buffer_data + frame_header_padding_ - msg_start); - uint16_t msg_len = static_cast(msg_header_len + msg.payload_size); + 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); } @@ -301,20 +301,23 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe assert(this->state_ == State::DATA); assert(!messages.empty()); #endif - uint8_t *buffer_data = buffer.get_buffer()->data(); StaticVector iovs; uint16_t total_write_len = 0; - const uint8_t padding = frame_header_padding_; for (const auto &msg : messages) { - uint8_t *msg_start = write_plaintext_header(buffer_data + msg.offset, msg, padding); - uint8_t msg_header_len = static_cast((buffer_data + msg.offset + padding) - msg_start); - size_t msg_len = static_cast(msg_header_len + msg.payload_size); + 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; } +#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); } diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 96d47e9c7bf..8314754715f 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -7,13 +7,15 @@ namespace esphome::api { class APIPlaintextFrameHelper final : public APIFrameHelper { public: + // Plaintext header structure (worst case): + // Pos 0: indicator (0x00) + // Pos 1-3: payload size varint (up to 3 bytes) + // Pos 4-5: message type varint (up to 2 bytes) + // Pos 6+: actual payload data + static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint + explicit APIPlaintextFrameHelper(std::unique_ptr socket) : APIFrameHelper(std::move(socket)) { - // Plaintext header structure (worst case): - // Pos 0: indicator (0x00) - // Pos 1-3: payload size varint (up to 3 bytes) - // Pos 4-5: message type varint (up to 2 bytes) - // Pos 6+: actual payload data - frame_header_padding_ = 6; + frame_header_padding_ = HEADER_PADDING; } ~APIPlaintextFrameHelper() override = default; APIError init() override;