#pragma once #include #include #include #include #include #include #include #include "esphome/core/defines.h" #ifdef USE_API #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" namespace esphome::api { // uncomment to log raw packets //#define HELPER_LOG_PACKETS // Maximum message size limits to prevent OOM on constrained devices // Handshake messages are limited to a small size for security static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128; // Data message limits vary by platform based on available memory #ifdef USE_ESP8266 static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 #else static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms #endif // Maximum number of messages to batch in a single write operation // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; class ProtoWriteBuffer; // Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; struct ReadPacketBuffer { const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; uint16_t type; }; // Packed message info structure to minimize memory usage struct MessageInfo { uint16_t offset; // Offset in buffer where message starts uint16_t payload_size; // Size of the message payload uint8_t message_type; // Message type (0-255) MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} }; enum class APIError : uint16_t { OK = 0, WOULD_BLOCK = 1001, BAD_INDICATOR = 1003, BAD_DATA_PACKET = 1004, TCP_NODELAY_FAILED = 1005, TCP_NONBLOCKING_FAILED = 1006, CLOSE_FAILED = 1007, SHUTDOWN_FAILED = 1008, BAD_STATE = 1009, BAD_ARG = 1010, SOCKET_READ_FAILED = 1011, SOCKET_WRITE_FAILED = 1012, OUT_OF_MEMORY = 1018, CONNECTION_CLOSED = 1022, #ifdef USE_API_NOISE BAD_HANDSHAKE_PACKET_LEN = 1002, HANDSHAKESTATE_READ_FAILED = 1013, HANDSHAKESTATE_WRITE_FAILED = 1014, HANDSHAKESTATE_BAD_STATE = 1015, CIPHERSTATE_DECRYPT_FAILED = 1016, CIPHERSTATE_ENCRYPT_FAILED = 1017, HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SPLIT_FAILED = 1020, BAD_HANDSHAKE_ERROR_BYTE = 1021, #endif }; const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; explicit APIFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} // Get client name (null-terminated) const char *get_client_name() const { return this->client_name_; } // Get client peername/IP (null-terminated, cached at init time for availability after socket failure) const char *get_client_peername() const { return this->client_peername_; } // Set client name from buffer with length (truncates if needed) void set_client_name(const char *name, size_t len) { size_t copy_len = std::min(len, sizeof(this->client_name_) - 1); memcpy(this->client_name_, name, copy_len); this->client_name_[copy_len] = '\0'; } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { state_ = State::CLOSED; int err = this->socket_->close(); if (err == -1) return APIError::CLOSE_FAILED; return APIError::OK; } APIError shutdown(int how) { int err = this->socket_->shutdown(how); if (err == -1) return APIError::SHUTDOWN_FAILED; if (how == SHUT_RDWR) { state_ = State::CLOSED; } return APIError::OK; } /// Toggle TCP_NODELAY socket option to control Nagle's algorithm. /// /// This is used to allow log messages to coalesce (Nagle enabled) while keeping /// state updates low-latency (NODELAY enabled). Without this, many small log /// packets fill the TCP send buffer, crowding out important state updates. /// /// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040) /// this is just a boolean assignment; on other platforms it's a lightweight syscall. /// /// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle /// @return true if successful or already in desired state bool set_nodelay(bool enable) { if (this->nodelay_enabled_ == enable) return true; int val = enable ? 1 : 0; int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); if (err == 0) { this->nodelay_enabled_ = enable; } return err == 0; } virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; // Write multiple protobuf messages in a single operation // messages contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) = 0; // Get the frame header padding required by this protocol uint8_t frame_header_padding() const { return frame_header_padding_; } // Get the frame footer size required by this protocol uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } // Release excess memory from internal buffers after initial sync void release_buffers() { // rx_buf_: Safe to clear only if no partial read in progress. // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame // and clearing would lose partially received data. if (this->rx_buf_len_ == 0) { // Use swap trick since shrink_to_fit() is non-binding and may be ignored std::vector().swap(this->rx_buf_); } } protected: // Buffer containing data to be sent struct SendBuffer { std::unique_ptr data; uint16_t size{0}; // Total size of the buffer uint16_t offset{0}; // Current offset within the buffer // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes uint16_t remaining() const { return size - offset; } const uint8_t *current_data() const { return data.get() + offset; } }; // Common implementation for writing raw data to socket APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); // Try to send data from the tx buffer APIError try_send_tx_buf_(); // Helper method to buffer data from IOVs void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset); // Common socket write error handling APIError handle_socket_write_error_(); template APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr socket_; // Common state enum for all frame helpers // Note: Not all states are used by all implementations // - INITIALIZE: Used by both Noise and Plaintext // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol // - DATA: Used by both Noise and Plaintext // - CLOSED: Used by both Noise and Plaintext // - FAILED: Used by both Noise and Plaintext // - EXPLICIT_REJECT: Only used by Noise protocol enum class State : uint8_t { INITIALIZE = 1, CLIENT_HELLO = 2, // Noise only SERVER_HELLO = 3, // Noise only HANDSHAKE = 4, // Noise only DATA = 5, CLOSED = 6, FAILED = 7, EXPLICIT_REJECT = 8, // Noise only }; // Containers (size varies, but typically 12+ bytes on 32-bit) std::array, API_MAX_SEND_QUEUE> tx_buf_; std::vector rx_buf_; // Client name buffer - stores name from Hello message or initial peername char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; // Cached peername/IP address - captured at init time for availability after socket failure char client_peername_[socket::SOCKADDR_STR_LEN]{}; // Group smaller types together uint16_t rx_buf_len_ = 0; State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; uint8_t tx_buf_head_{0}; uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; // Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true // since init_common_() enables NODELAY. Used by set_nodelay() to allow log // messages to coalesce while keeping state updates low-latency. bool nodelay_enabled_{true}; // Common initialization for both plaintext and noise protocols APIError init_common_(); // Helper method to handle socket read results APIError handle_socket_read_result_(ssize_t received); }; } // namespace esphome::api #endif // USE_API