ESPHome 2026.7.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_PER_BATCH 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// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits.
53// The noise wire format encodes types as 16-bit, but the high byte is always 0.
54// If message types ever exceed 255, this and encrypt_noise_message_ must be updated.
56 uint16_t offset; // Offset in buffer where message starts
57 uint16_t payload_size; // Size of the message payload
58 uint8_t message_type; // Message type (0-255)
59 uint8_t header_size; // Actual header size used (avoids recomputation in write path)
60
61 MessageInfo(uint8_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 (8×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, uint8_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::varint8(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::varint8(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 }
222 }
223
224 protected:
225 // Drain backlogged overflow data to the socket and handle errors.
226 // Called when overflow_buf_.empty() is false. Out-of-line to keep the
227 // fast path (empty check) inline at call sites.
228 // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
230
231 // Sentinel values for the sent parameter in write_raw_ methods
232 static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1
233 static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
234
235 // Dispatch to write() or writev() based on iovec count
236 inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
237 return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
238 }
239
240 // Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
241 // These inline the fast path (overflow empty + full write) and tail-call the out-of-line
242 // slow path only on failure/partial write.
243 inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
244 if (this->overflow_buf_.empty()) [[likely]] {
245 ssize_t sent = this->socket_->write(data, len);
246 if (sent == static_cast<ssize_t>(len)) [[likely]] {
247#ifdef HELPER_LOG_PACKETS
248 this->log_packet_sending_(data, len);
249#endif
250 return APIError::OK;
251 }
252 // sent is -1 (WRITE_FAILED) or partial write count
253 return this->write_raw_buf_(data, len, sent);
254 }
255 return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED);
256 }
257 // Out-of-line write paths: handle partial writes, errors, overflow buffering
258 // sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write)
259 APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED);
260 APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
262#ifdef HELPER_LOG_PACKETS
263 void log_packet_sending_(const void *data, uint16_t len);
264#endif
265
266 // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
267 std::unique_ptr<socket::Socket> socket_;
268
269 // Common state enum for all frame helpers
270 // Note: Not all states are used by all implementations
271 // - INITIALIZE: Used by both Noise and Plaintext
272 // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
273 // - DATA: Used by both Noise and Plaintext
274 // - CLOSED: Used by both Noise and Plaintext
275 // - FAILED: Used by both Noise and Plaintext
276 // - EXPLICIT_REJECT: Only used by Noise protocol
277 enum class State : uint8_t {
278 INITIALIZE = 1,
279 CLIENT_HELLO = 2, // Noise only
280 SERVER_HELLO = 3, // Noise only
281 HANDSHAKE = 4, // Noise only
282 DATA = 5,
283 CLOSED = 6,
284 FAILED = 7,
285 EXPLICIT_REJECT = 8, // Noise only
286 };
287
288 // Fast inline state check for read_packet/write_protobuf_messages hot path.
289 // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
290 // other intermediate state to WOULD_BLOCK.
291 inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
292 if (this->state_ == State::DATA)
293 return APIError::OK;
294 if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
295 return APIError::BAD_STATE;
297 }
298
299 // Backlog for unsent data when TCP send buffer is full (rarely used in production)
302
303 // Client name buffer - stores name from Hello message or initial peername
304 char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
305
306 // Group smaller types together
307 uint16_t rx_buf_len_ = 0;
311 // Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
312 // Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
313 // After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
314 // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
315 // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
316#ifdef USE_ESP8266
317 static constexpr uint8_t LOG_NAGLE_COUNT = 2;
318#else
319 static constexpr uint8_t LOG_NAGLE_COUNT = 3;
320#endif
322
323 // Internal helper to set TCP_NODELAY socket option
324 void set_nodelay_raw_(bool enable) {
325 int val = enable ? 1 : 0;
326 this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
327 }
328
329 // Common initialization for both plaintext and noise protocols
331
332 // Helper method to handle socket read results
334};
335
336} // namespace esphome::api
337
338#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:60
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
uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const
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_
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)
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer)=0
APIError ESPHOME_ALWAYS_INLINE check_data_state_() const
void set_nodelay_for_message(bool is_log_message)
virtual ~APIFrameHelper()=default
ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt)
Circular queue of heap-allocated byte buffers used as a TCP send backlog.
bool empty() const
True when no backlogged data is waiting.
static constexpr uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value)
Definition proto.h:688
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(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
uint32_t payload_size()