ESPHome 2026.3.3
Loading...
Searching...
No Matches
api_frame_helper.h
Go to the documentation of this file.
1#pragma once
2#include <array>
3#include <cstdint>
4#include <limits>
5#include <memory>
6#include <span>
7#include <utility>
8
10#ifdef USE_API
14#include "esphome/core/log.h"
15
16namespace esphome::api {
17
18// uncomment to log raw packets
19//#define HELPER_LOG_PACKETS
20
21// Maximum message size limits to prevent OOM on constrained devices
22// Handshake messages are limited to a small size for security
23static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
24
25// Data message limits vary by platform based on available memory
26#ifdef USE_ESP8266
27static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
28#else
29static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
30#endif
31
32// Extra byte reserved in rx_buf_ beyond the message size so protobuf
33// StringRef fields can be null-terminated in-place after decode.
34static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
35
36// Maximum number of messages to batch in a single write operation
37// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
38static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
39
40class ProtoWriteBuffer;
41
42// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
43static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
44
46 const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call)
47 uint16_t data_len;
48 uint16_t type;
49};
50
51// Packed message info structure to minimize memory usage
53 uint16_t offset; // Offset in buffer where message starts
54 uint16_t payload_size; // Size of the message payload
55 uint8_t message_type; // Message type (0-255)
56
57 MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
58};
59
60enum class APIError : uint16_t {
61 OK = 0,
62 WOULD_BLOCK = 1001,
63 BAD_INDICATOR = 1003,
64 BAD_DATA_PACKET = 1004,
65 TCP_NODELAY_FAILED = 1005,
67 CLOSE_FAILED = 1007,
68 SHUTDOWN_FAILED = 1008,
69 BAD_STATE = 1009,
70 BAD_ARG = 1010,
71 SOCKET_READ_FAILED = 1011,
73 OUT_OF_MEMORY = 1018,
74 CONNECTION_CLOSED = 1022,
75#ifdef USE_API_NOISE
85#endif
86};
87
88const LogString *api_error_to_logstr(APIError err);
89
91 public:
92 APIFrameHelper() = default;
93 explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
94
95 // Get client name (null-terminated)
96 const char *get_client_name() const { return this->client_name_; }
97 // Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
98 // Returns pointer to buf for convenience in printf-style calls
99 const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
100 // Set client name from buffer with length (truncates if needed)
101 void set_client_name(const char *name, size_t len) {
102 size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
103 memcpy(this->client_name_, name, copy_len);
104 this->client_name_[copy_len] = '\0';
105 }
106 virtual ~APIFrameHelper() = default;
107 virtual APIError init() = 0;
108 virtual APIError loop();
110 bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
111 int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
113 if (state_ == State::CLOSED)
114 return APIError::OK; // Already closed
116 int err = this->socket_->close();
117 if (err == -1)
119 return APIError::OK;
120 }
122 int err = this->socket_->shutdown(how);
123 if (err == -1)
125 if (how == SHUT_RDWR) {
127 }
128 return APIError::OK;
129 }
130 // Manage TCP_NODELAY (Nagle's algorithm) based on message type.
131 //
132 // For non-log messages (sensor data, state updates): Always disable Nagle
133 // (NODELAY on) for immediate delivery - these are time-sensitive.
134 //
135 // For log messages: Use Nagle to coalesce multiple small log packets into
136 // fewer larger packets, reducing WiFi overhead. However, we limit batching
137 // to avoid excessive LWIP buffer pressure on memory-constrained devices.
138 // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but
139 // holding data too long waiting for Nagle's timer causes buffer exhaustion
140 // and dropped messages.
141 //
142 // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle
143 // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers)
144 //
145 // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush)
146 // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all)
147 //
148 void set_nodelay_for_message(bool is_log_message) {
149 if (!is_log_message) {
150 if (this->nodelay_state_ != NODELAY_ON) {
151 this->set_nodelay_raw_(true);
153 }
154 return;
155 }
156
157 // Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush)
158 if (this->nodelay_state_ == NODELAY_ON) {
159 this->set_nodelay_raw_(false);
160 this->nodelay_state_ = 1;
161 } else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
162 this->set_nodelay_raw_(true);
164 } else {
165 this->nodelay_state_++;
166 }
167 }
169 // Write multiple protobuf messages in a single operation
170 // messages contains (message_type, offset, length) for each message in the buffer
171 // The buffer contains all messages with appropriate padding before each
172 virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
173 // Get the frame header padding required by this protocol
174 uint8_t frame_header_padding() const { return frame_header_padding_; }
175 // Get the frame footer size required by this protocol
176 uint8_t frame_footer_size() const { return frame_footer_size_; }
177 // Check if socket has data ready to read
178 bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
179 // Release excess memory from internal buffers after initial sync
181 // rx_buf_: Safe to clear only if no partial read in progress.
182 // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
183 // and clearing would lose partially received data.
184 if (this->rx_buf_len_ == 0) {
185 this->rx_buf_.release();
186 }
187 }
188
189 protected:
190 // Buffer containing data to be sent
191 struct SendBuffer {
192 std::unique_ptr<uint8_t[]> data;
193 uint16_t size{0}; // Total size of the buffer
194 uint16_t offset{0}; // Current offset within the buffer
195
196 // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
197 uint16_t remaining() const { return size - offset; }
198 const uint8_t *current_data() const { return data.get() + offset; }
199 };
200
201 // Common implementation for writing raw data to socket
202 APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
203
204 // Try to send data from the tx buffer
206
207 // Helper method to buffer data from IOVs
208 void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
209
210 // Common socket write error handling
212
213 // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
214 std::unique_ptr<socket::Socket> socket_;
215
216 // Common state enum for all frame helpers
217 // Note: Not all states are used by all implementations
218 // - INITIALIZE: Used by both Noise and Plaintext
219 // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
220 // - DATA: Used by both Noise and Plaintext
221 // - CLOSED: Used by both Noise and Plaintext
222 // - FAILED: Used by both Noise and Plaintext
223 // - EXPLICIT_REJECT: Only used by Noise protocol
224 enum class State : uint8_t {
225 INITIALIZE = 1,
226 CLIENT_HELLO = 2, // Noise only
227 SERVER_HELLO = 3, // Noise only
228 HANDSHAKE = 4, // Noise only
229 DATA = 5,
230 CLOSED = 6,
231 FAILED = 7,
232 EXPLICIT_REJECT = 8, // Noise only
233 };
234
235 // Fast inline state check for read_packet/write_protobuf_messages hot path.
236 // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
237 // other intermediate state to WOULD_BLOCK.
238 inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
239 if (this->state_ == State::DATA)
240 return APIError::OK;
241 if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
242 return APIError::BAD_STATE;
244 }
245
246 // Containers (size varies, but typically 12+ bytes on 32-bit)
247 std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
249
250 // Client name buffer - stores name from Hello message or initial peername
251 char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
252
253 // Group smaller types together
254 uint16_t rx_buf_len_ = 0;
258 uint8_t tx_buf_head_{0};
259 uint8_t tx_buf_tail_{0};
260 uint8_t tx_buf_count_{0};
261 // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
262 // (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
263 // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
264 // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
265 // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
266 static constexpr int8_t NODELAY_ON = -1;
267#ifdef USE_ESP8266
268 static constexpr int8_t LOG_NAGLE_COUNT = 2;
269#else
270 static constexpr int8_t LOG_NAGLE_COUNT = 3;
271#endif
273
274 // Internal helper to set TCP_NODELAY socket option
275 void set_nodelay_raw_(bool enable) {
276 int val = enable ? 1 : 0;
277 this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
278 }
279
280 // Common initialization for both plaintext and noise protocols
282
283 // Helper method to handle socket read results
285};
286
287} // namespace esphome::api
288
289#endif // USE_API
Byte buffer that skips zero-initialization on resize().
Definition api_buffer.h:36
void release()
Release all memory (equivalent to std::vector swap trick).
Definition api_buffer.h:54
const char * get_client_name() const
APIError handle_socket_read_result_(ssize_t received)
virtual APIError read_packet(ReadPacketBuffer *buffer)=0
std::array< std::unique_ptr< SendBuffer >, API_MAX_SEND_QUEUE > tx_buf_
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset)
int getpeername(struct sockaddr *addr, socklen_t *addrlen)
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span< const MessageInfo > messages)=0
virtual APIError init()=0
APIFrameHelper(std::unique_ptr< socket::Socket > socket)
const char * get_peername_to(std::span< char, socket::SOCKADDR_STR_LEN > buf) const
static constexpr int8_t LOG_NAGLE_COUNT
std::unique_ptr< socket::Socket > socket_
char client_name_[CLIENT_INFO_NAME_MAX_LEN]
void set_client_name(const char *name, size_t len)
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer)=0
APIError ESPHOME_ALWAYS_INLINE check_data_state_() const
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len)
static constexpr int8_t NODELAY_ON
void set_nodelay_for_message(bool is_log_message)
virtual ~APIFrameHelper()=default
uint16_t type
uint32_t socklen_t
Definition headers.h:99
__int64 ssize_t
Definition httplib.h:178
mopeka_std_values val[3]
const LogString * api_error_to_logstr(APIError err)
std::string size_t len
Definition helpers.h:892
size_t size
Definition helpers.h:929
MessageInfo(uint8_t type, uint16_t off, uint16_t size)