ESPHome 2026.9.0
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
15#include "esphome/core/log.h"
16#include "proto.h"
17
18namespace esphome::api {
19
20// uncomment to log raw packets
21//#define HELPER_LOG_PACKETS
22
23// Maximum message size limits to prevent OOM on constrained devices
24// Handshake messages are limited to a small size for security
25static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
26
27// Data message limits vary by platform based on available memory
28#ifdef USE_ESP8266
29static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
30#else
31static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
32#endif
33
34// Extra byte reserved in rx_buf_ beyond the message size so protobuf
35// StringRef fields can be null-terminated in-place after decode.
36static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
37
38// Maximum number of messages to batch in a single write operation
39// Must be >= MAX_INITIAL_BATCH_SIZE in api_connection.h (enforced by static_assert there)
40static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
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
52// message_type matches the wire formats: noise carries a fixed 16-bit type
53// field, plaintext a type varint. The proto codegen caps message IDs at 16383
54// so the plaintext type varint fits the 2 bytes budgeted in HEADER_PADDING.
56 uint16_t offset; // Offset in buffer where message starts
57 uint16_t payload_size; // Size of the message payload
58 uint16_t message_type; // Message type (0-16383)
59 uint8_t header_size; // Actual header size used (avoids recomputation in write path)
60
61 MessageInfo(uint16_t type, uint16_t off, uint16_t size, uint8_t hdr)
63};
64
65enum class APIError : uint16_t {
66 OK = 0,
67 WOULD_BLOCK = 1001,
68 BAD_INDICATOR = 1003,
69 BAD_DATA_PACKET = 1004,
70 TCP_NODELAY_FAILED = 1005,
72 CLOSE_FAILED = 1007,
73 SHUTDOWN_FAILED = 1008,
74 BAD_STATE = 1009,
75 BAD_ARG = 1010,
76 SOCKET_READ_FAILED = 1011,
78 OUT_OF_MEMORY = 1018,
79 CONNECTION_CLOSED = 1022,
80#ifdef USE_API_NOISE
90#endif
91#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
92 // Not an error: an unprovisioned device received a Noise client hello on a
93 // plaintext connection; the caller must hand the socket off to a Noise helper.
95#endif
96};
97
98const LogString *api_error_to_logstr(APIError err);
99
101 public:
102 APIFrameHelper() = default;
103 explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
104
105 // Get client name (null-terminated)
106 const char *get_client_name() const { return this->client_name_; }
107 // Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
108 // Returns pointer to buf for convenience in printf-style calls
109 const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
110 // Set client name from buffer with length (truncates if needed)
111 void set_client_name(const char *name, size_t len) {
112 size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
113 memcpy(this->client_name_, name, copy_len);
114 this->client_name_[copy_len] = '\0';
115 }
116 virtual ~APIFrameHelper() = default;
117 virtual APIError init() = 0;
118 virtual APIError loop() = 0;
120 bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
121 int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
123 if (state_ == State::CLOSED)
124 return APIError::OK; // Already closed
126 int err = this->socket_->close();
127 if (err == -1)
129 return APIError::OK;
130 }
132 int err = this->socket_->shutdown(how);
133 if (err == -1)
135 if (how == SHUT_RDWR) {
137 }
138 return APIError::OK;
139 }
140 // Manage TCP_NODELAY (Nagle's algorithm) based on message type.
141 //
142 // For non-log messages (sensor data, state updates): Always disable Nagle
143 // (NODELAY on) for immediate delivery - these are time-sensitive.
144 //
145 // For log messages: Use Nagle to coalesce multiple small log packets into
146 // fewer larger packets, reducing WiFi overhead. However, we limit batching
147 // to avoid excessive LWIP buffer pressure on memory-constrained devices.
148 // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but
149 // holding data too long waiting for Nagle's timer causes buffer exhaustion
150 // and dropped messages.
151 //
152 // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (4×MSS) / LibreTiny (4×MSS): 4 logs per cycle
153 // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers)
154 //
155 // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush)
156 // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all)
157 //
158 void set_nodelay_for_message(bool is_log_message) {
159 if (!is_log_message) {
160 if (this->nodelay_counter_) {
161 this->set_nodelay_raw_(true);
162 this->nodelay_counter_ = 0;
163 }
164 return;
165 }
166 // Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT
167 if (!this->nodelay_counter_)
168 this->set_nodelay_raw_(false);
169 if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) {
170 this->set_nodelay_raw_(true);
171 this->nodelay_counter_ = 0;
172 }
173 }
174 // Write a single protobuf message - the hot path (87-100% of all writes).
175 // Caller must ensure state is DATA before calling.
177 // Write multiple protobuf messages in a single batched operation.
178 // Caller must ensure state is DATA and messages is not empty.
179 // messages contains (message_type, offset, length) for each message in the buffer.
180 // The buffer contains all messages with appropriate padding before each.
181 virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
182 // Get the maximum frame header padding required by this protocol (worst case)
183 uint8_t frame_header_padding() const { return frame_header_padding_; }
184 // Get the actual frame header size for a specific message.
185 // For noise: always returns frame_header_padding_ (fixed 7-byte header).
186 // For plaintext: computes actual size from varint lengths (3-6 bytes).
187 // Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
188 // footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
189 // added, this should become a virtual method.
190 uint8_t frame_header_size(uint16_t payload_size, uint16_t message_type) const {
191#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
192 return this->frame_footer_size_
194 : static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(message_type));
195#elif defined(USE_API_NOISE)
196 return this->frame_header_padding_;
197#else // USE_API_PLAINTEXT only
198 return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(message_type));
199#endif
200 }
201 // Get the frame footer size required by this protocol
202 uint8_t frame_footer_size() const { return frame_footer_size_; }
203 // Check if socket has buffered data ready to read.
204 // Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
205 // or track that they stopped early and retry without this check.
206 // See Socket::ready() for details.
207 bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
208#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
209 // Move the socket out of this helper so a replacement helper can take it
210 // over (plaintext to Noise handoff on unprovisioned devices). The drained
211 // helper must be destroyed right after.
212 std::unique_ptr<socket::Socket> release_socket_for_switch() { return std::move(this->socket_); }
213#endif
214 // Release excess memory from internal buffers after initial sync
216 // rx_buf_: Safe to clear only if no partial read in progress.
217 // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
218 // and clearing would lose partially received data.
219 if (this->rx_buf_len_ == 0) {
220 this->rx_buf_.release();
221 }
223 }
224 // Free the send backlog storage once it has drained
226
227 protected:
228 // Drain backlogged overflow data to the socket and handle errors.
229 // Called when overflow_buf_.empty() is false. Out-of-line to keep the
230 // fast path (empty check) inline at call sites.
231 // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
233
234 // Sentinel values for the sent parameter in write_raw_ methods
235 static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1
236 static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
237
238 // Dispatch to write() or writev() based on iovec count
239 inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
240 return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
241 }
242
243 // Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
244 // These inline the fast path (overflow empty + full write) and tail-call the out-of-line
245 // slow path only on failure/partial write.
246 inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
247 if (this->overflow_buf_.empty()) [[likely]] {
248 ssize_t sent = this->socket_->write(data, len);
249 if (sent == static_cast<ssize_t>(len)) [[likely]] {
250#ifdef HELPER_LOG_PACKETS
251 this->log_packet_sending_(data, len);
252#endif
253 return APIError::OK;
254 }
255 // sent is -1 (WRITE_FAILED) or partial write count
256 return this->write_raw_buf_(data, len, sent);
257 }
258 return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED);
259 }
260 // Out-of-line write paths: handle partial writes, errors, overflow buffering
261 // sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write)
262 APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED);
263 APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
265#ifdef HELPER_LOG_PACKETS
266 void log_packet_sending_(const void *data, uint16_t len);
267#endif
268
269 // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
270 std::unique_ptr<socket::Socket> socket_;
271
272 // Common state enum for all frame helpers
273 // Note: Not all states are used by all implementations
274 // - INITIALIZE: Used by both Noise and Plaintext
275 // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
276 // - DATA: Used by both Noise and Plaintext
277 // - CLOSED: Used by both Noise and Plaintext
278 // - FAILED: Used by both Noise and Plaintext
279 // - EXPLICIT_REJECT: Only used by Noise protocol
280 enum class State : uint8_t {
281 INITIALIZE = 1,
282 CLIENT_HELLO = 2, // Noise only
283 SERVER_HELLO = 3, // Noise only
284 HANDSHAKE = 4, // Noise only
285 DATA = 5,
286 CLOSED = 6,
287 FAILED = 7,
288 EXPLICIT_REJECT = 8, // Noise only
289 };
290
291 // Fast inline state check for read_packet/write_protobuf_messages hot path.
292 // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
293 // other intermediate state to WOULD_BLOCK.
294 inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
295 if (this->state_ == State::DATA)
296 return APIError::OK;
297 if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
298 return APIError::BAD_STATE;
300 }
301
302 // Backlog for unsent data when TCP send buffer is full (rarely used in production)
305
306 // Client name buffer - stores name from Hello message or initial peername
307 char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
308
309 // Group smaller types together
310 uint16_t rx_buf_len_ = 0;
314 // Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
315 // Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
316 // After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
317 // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
318 // ESP32 (4×MSS+), RP2040 (4×MSS), and LibreTiny (4×MSS) can coalesce more.
319#ifdef USE_ESP8266
320 static constexpr uint8_t LOG_NAGLE_COUNT = 2;
321#else
322 static constexpr uint8_t LOG_NAGLE_COUNT = 3;
323#endif
325
326 // Internal helper to set TCP_NODELAY socket option
327 void set_nodelay_raw_(bool enable) {
328 int val = enable ? 1 : 0;
329 this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
330 }
331
332 // Common initialization for both plaintext and noise protocols
334
335 // Helper method to handle socket read results
337};
338
339} // namespace esphome::api
340
341#endif // USE_API
Byte buffer that skips zero-initialization on resize().
Definition api_buffer.h:26
void release()
Release all memory (equivalent to std::vector swap trick).
Definition api_buffer.h:61
const char * get_client_name() const
APIError handle_socket_read_result_(ssize_t received)
APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len)
void log_packet_sending_(const void *data, uint16_t len)
virtual APIError read_packet(ReadPacketBuffer *buffer)=0
std::unique_ptr< socket::Socket > release_socket_for_switch()
int getpeername(struct sockaddr *addr, socklen_t *addrlen)
virtual APIError loop()=0
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span< const MessageInfo > messages)=0
virtual APIError init()=0
APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent=WRITE_NOT_ATTEMPTED)
static constexpr ssize_t WRITE_FAILED
APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent=WRITE_NOT_ATTEMPTED)
APIFrameHelper(std::unique_ptr< socket::Socket > socket)
static constexpr ssize_t WRITE_NOT_ATTEMPTED
const char * get_peername_to(std::span< char, socket::SOCKADDR_STR_LEN > buf) const
std::unique_ptr< socket::Socket > socket_
uint8_t frame_header_size(uint16_t payload_size, uint16_t message_type) const
char client_name_[CLIENT_INFO_NAME_MAX_LEN]
static constexpr uint8_t LOG_NAGLE_COUNT
void set_client_name(const char *name, size_t len)
APIError ESPHOME_ALWAYS_INLINE check_data_state_() const
void set_nodelay_for_message(bool is_log_message)
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer)=0
virtual ~APIFrameHelper()=default
ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt)
TCP send backlog, only used when the socket send buffer is full.
void release()
Free the retained storage, now if empty, otherwise once it has drained.
bool empty() const
True when no backlogged data is waiting.
static constexpr uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value)
Definition proto.h:683
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)
const void size_t len
Definition hal.h:64
uint16_t size
Definition helpers.cpp:25
STL namespace.
MessageInfo(uint16_t type, uint16_t off, uint16_t size, uint8_t hdr)
uint32_t payload_size()