ESPHome 2026.5.0-dev
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};
92
93const LogString *api_error_to_logstr(APIError err);
94
96 public:
97 APIFrameHelper() = default;
98 explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
99
100 // Get client name (null-terminated)
101 const char *get_client_name() const { return this->client_name_; }
102 // Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
103 // Returns pointer to buf for convenience in printf-style calls
104 const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
105 // Set client name from buffer with length (truncates if needed)
106 void set_client_name(const char *name, size_t len) {
107 size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
108 memcpy(this->client_name_, name, copy_len);
109 this->client_name_[copy_len] = '\0';
110 }
111 virtual ~APIFrameHelper() = default;
112 virtual APIError init() = 0;
113 virtual APIError loop() = 0;
115 bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
116 int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
118 if (state_ == State::CLOSED)
119 return APIError::OK; // Already closed
121 int err = this->socket_->close();
122 if (err == -1)
124 return APIError::OK;
125 }
127 int err = this->socket_->shutdown(how);
128 if (err == -1)
130 if (how == SHUT_RDWR) {
132 }
133 return APIError::OK;
134 }
135 // Manage TCP_NODELAY (Nagle's algorithm) based on message type.
136 //
137 // For non-log messages (sensor data, state updates): Always disable Nagle
138 // (NODELAY on) for immediate delivery - these are time-sensitive.
139 //
140 // For log messages: Use Nagle to coalesce multiple small log packets into
141 // fewer larger packets, reducing WiFi overhead. However, we limit batching
142 // to avoid excessive LWIP buffer pressure on memory-constrained devices.
143 // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but
144 // holding data too long waiting for Nagle's timer causes buffer exhaustion
145 // and dropped messages.
146 //
147 // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle
148 // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers)
149 //
150 // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush)
151 // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all)
152 //
153 void set_nodelay_for_message(bool is_log_message) {
154 if (!is_log_message) {
155 if (this->nodelay_counter_) {
156 this->set_nodelay_raw_(true);
157 this->nodelay_counter_ = 0;
158 }
159 return;
160 }
161 // Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT
162 if (!this->nodelay_counter_)
163 this->set_nodelay_raw_(false);
164 if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) {
165 this->set_nodelay_raw_(true);
166 this->nodelay_counter_ = 0;
167 }
168 }
169 // Write a single protobuf message - the hot path (87-100% of all writes).
170 // Caller must ensure state is DATA before calling.
172 // Write multiple protobuf messages in a single batched operation.
173 // Caller must ensure state is DATA and messages is not empty.
174 // messages contains (message_type, offset, length) for each message in the buffer.
175 // The buffer contains all messages with appropriate padding before each.
176 virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
177 // Get the maximum frame header padding required by this protocol (worst case)
178 uint8_t frame_header_padding() const { return frame_header_padding_; }
179 // Get the actual frame header size for a specific message.
180 // For noise: always returns frame_header_padding_ (fixed 7-byte header).
181 // For plaintext: computes actual size from varint lengths (3-6 bytes).
182 // Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
183 // footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
184 // added, this should become a virtual method.
185 uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const {
186#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
187 return this->frame_footer_size_
189 : static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
190#elif defined(USE_API_NOISE)
191 return this->frame_header_padding_;
192#else // USE_API_PLAINTEXT only
193 return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
194#endif
195 }
196 // Get the frame footer size required by this protocol
197 uint8_t frame_footer_size() const { return frame_footer_size_; }
198 // Check if socket has buffered data ready to read.
199 // Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
200 // or track that they stopped early and retry without this check.
201 // See Socket::ready() for details.
202 bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
203 // Release excess memory from internal buffers after initial sync
205 // rx_buf_: Safe to clear only if no partial read in progress.
206 // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
207 // and clearing would lose partially received data.
208 if (this->rx_buf_len_ == 0) {
209 this->rx_buf_.release();
210 }
211 }
212
213 protected:
214 // Drain backlogged overflow data to the socket and handle errors.
215 // Called when overflow_buf_.empty() is false. Out-of-line to keep the
216 // fast path (empty check) inline at call sites.
217 // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
219
220 // Sentinel values for the sent parameter in write_raw_ methods
221 static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1
222 static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
223
224 // Dispatch to write() or writev() based on iovec count
225 inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
226 return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
227 }
228
229 // Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
230 // These inline the fast path (overflow empty + full write) and tail-call the out-of-line
231 // slow path only on failure/partial write.
232 inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
233 if (this->overflow_buf_.empty()) [[likely]] {
234 ssize_t sent = this->socket_->write(data, len);
235 if (sent == static_cast<ssize_t>(len)) [[likely]] {
236#ifdef HELPER_LOG_PACKETS
237 this->log_packet_sending_(data, len);
238#endif
239 return APIError::OK;
240 }
241 // sent is -1 (WRITE_FAILED) or partial write count
242 return this->write_raw_buf_(data, len, sent);
243 }
244 return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED);
245 }
246 // Out-of-line write paths: handle partial writes, errors, overflow buffering
247 // sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write)
248 APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED);
249 APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
251#ifdef HELPER_LOG_PACKETS
252 void log_packet_sending_(const void *data, uint16_t len);
253#endif
254
255 // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
256 std::unique_ptr<socket::Socket> socket_;
257
258 // Common state enum for all frame helpers
259 // Note: Not all states are used by all implementations
260 // - INITIALIZE: Used by both Noise and Plaintext
261 // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
262 // - DATA: Used by both Noise and Plaintext
263 // - CLOSED: Used by both Noise and Plaintext
264 // - FAILED: Used by both Noise and Plaintext
265 // - EXPLICIT_REJECT: Only used by Noise protocol
266 enum class State : uint8_t {
267 INITIALIZE = 1,
268 CLIENT_HELLO = 2, // Noise only
269 SERVER_HELLO = 3, // Noise only
270 HANDSHAKE = 4, // Noise only
271 DATA = 5,
272 CLOSED = 6,
273 FAILED = 7,
274 EXPLICIT_REJECT = 8, // Noise only
275 };
276
277 // Fast inline state check for read_packet/write_protobuf_messages hot path.
278 // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
279 // other intermediate state to WOULD_BLOCK.
280 inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
281 if (this->state_ == State::DATA)
282 return APIError::OK;
283 if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
284 return APIError::BAD_STATE;
286 }
287
288 // Backlog for unsent data when TCP send buffer is full (rarely used in production)
291
292 // Client name buffer - stores name from Hello message or initial peername
293 char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
294
295 // Group smaller types together
296 uint16_t rx_buf_len_ = 0;
300 // Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
301 // Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
302 // After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
303 // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
304 // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
305#ifdef USE_ESP8266
306 static constexpr uint8_t LOG_NAGLE_COUNT = 2;
307#else
308 static constexpr uint8_t LOG_NAGLE_COUNT = 3;
309#endif
311
312 // Internal helper to set TCP_NODELAY socket option
313 void set_nodelay_raw_(bool enable) {
314 int val = enable ? 1 : 0;
315 this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
316 }
317
318 // Common initialization for both plaintext and noise protocols
320
321 // Helper method to handle socket read results
323};
324
325} // namespace esphome::api
326
327#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
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:661
static constexpr uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value)
Definition proto.h:656
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:1045
uint16_t size
Definition helpers.cpp:25
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
uint32_t payload_size()