ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
api_frame_helper.cpp
Go to the documentation of this file.
1#include "api_frame_helper.h"
2#ifdef USE_API
4#include "esphome/core/hal.h"
6#include "esphome/core/log.h"
7#include "proto.h"
8#include <cstring>
9#include <cinttypes>
10
11namespace esphome::api {
12
13static const char *const TAG = "api.frame_helper";
14
15// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
16static constexpr size_t API_MAX_LOG_BYTES = 168;
17
18#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
19#define HELPER_LOG(msg, ...) \
20 do { \
21 char peername_buf[socket::SOCKADDR_STR_LEN]; \
22 this->get_peername_to(peername_buf); \
23 ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
24 } while (0)
25#else
26#define HELPER_LOG(msg, ...) ((void) 0)
27#endif
28
29#ifdef HELPER_LOG_PACKETS
30#define LOG_PACKET_RECEIVED(buffer) \
31 do { \
32 char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
33 ESP_LOGVV(TAG, "Received frame: %s", \
34 format_hex_pretty_to(hex_buf_, (buffer).data(), \
35 (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
36 } while (0)
37#define LOG_PACKET_SENDING(data, len) \
38 do { \
39 char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
40 ESP_LOGVV(TAG, "Sending raw: %s", \
41 format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
42 } while (0)
43#else
44#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
45#define LOG_PACKET_SENDING(data, len) ((void) 0)
46#endif
47
48const LogString *api_error_to_logstr(APIError err) {
49 // not using switch to ensure compiler doesn't try to build a big table out of it
50 if (err == APIError::OK) {
51 return LOG_STR("OK");
52 } else if (err == APIError::WOULD_BLOCK) {
53 return LOG_STR("WOULD_BLOCK");
54 } else if (err == APIError::BAD_INDICATOR) {
55 return LOG_STR("BAD_INDICATOR");
56 } else if (err == APIError::BAD_DATA_PACKET) {
57 return LOG_STR("BAD_DATA_PACKET");
58 } else if (err == APIError::TCP_NODELAY_FAILED) {
59 return LOG_STR("TCP_NODELAY_FAILED");
60 } else if (err == APIError::TCP_NONBLOCKING_FAILED) {
61 return LOG_STR("TCP_NONBLOCKING_FAILED");
62 } else if (err == APIError::CLOSE_FAILED) {
63 return LOG_STR("CLOSE_FAILED");
64 } else if (err == APIError::SHUTDOWN_FAILED) {
65 return LOG_STR("SHUTDOWN_FAILED");
66 } else if (err == APIError::BAD_STATE) {
67 return LOG_STR("BAD_STATE");
68 } else if (err == APIError::BAD_ARG) {
69 return LOG_STR("BAD_ARG");
70 } else if (err == APIError::SOCKET_READ_FAILED) {
71 return LOG_STR("SOCKET_READ_FAILED");
72 } else if (err == APIError::SOCKET_WRITE_FAILED) {
73 return LOG_STR("SOCKET_WRITE_FAILED");
74 } else if (err == APIError::OUT_OF_MEMORY) {
75 return LOG_STR("OUT_OF_MEMORY");
76 } else if (err == APIError::CONNECTION_CLOSED) {
77 return LOG_STR("CONNECTION_CLOSED");
78 }
79#ifdef USE_API_NOISE
80 else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) {
81 return LOG_STR("BAD_HANDSHAKE_PACKET_LEN");
82 } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) {
83 return LOG_STR("HANDSHAKESTATE_READ_FAILED");
84 } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) {
85 return LOG_STR("HANDSHAKESTATE_WRITE_FAILED");
86 } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) {
87 return LOG_STR("HANDSHAKESTATE_BAD_STATE");
88 } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) {
89 return LOG_STR("CIPHERSTATE_DECRYPT_FAILED");
90 } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) {
91 return LOG_STR("CIPHERSTATE_ENCRYPT_FAILED");
92 } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) {
93 return LOG_STR("HANDSHAKESTATE_SETUP_FAILED");
94 } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) {
95 return LOG_STR("HANDSHAKESTATE_SPLIT_FAILED");
96 } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) {
97 return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE");
98 }
99#endif
100 // PROTOCOL_SWITCH_TO_NOISE is intercepted in APIConnection::loop() before
101 // any logging can happen, so it intentionally has no entry here.
102 return LOG_STR("UNKNOWN");
103}
104
105#ifdef HELPER_LOG_PACKETS
106void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) {
107 LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
108}
109#endif
110
112 if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
113 int err = errno;
114 if (err != EWOULDBLOCK && err != EAGAIN) {
115 this->state_ = State::FAILED;
116 HELPER_LOG("Socket write failed with errno %d", err);
118 }
119 }
120 return APIError::OK;
121}
122
123// Single-buffer write path: wraps in iovec and delegates.
124APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) {
125 struct iovec iov = {const_cast<void *>(data), len};
126 APIError err = this->write_raw_iov_(&iov, 1, len, sent);
127#ifdef HELPER_LOG_PACKETS
128 // Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent
129 if (err == APIError::OK)
130 LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
131#endif
132 return err;
133}
134
135// Handles partial writes, errors, and overflow buffering.
136// Called when the inline fast path couldn't complete the write,
137// or directly from cold paths (handshake, error handling).
138APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) {
139 if (sent <= 0) {
140 if (sent == WRITE_NOT_ATTEMPTED) {
141 // Cold path: no write attempted yet, drain overflow and try
142 if (!this->overflow_buf_.empty()) {
144 if (err != APIError::OK)
145 return err;
146 }
147 if (this->overflow_buf_.empty()) {
148 sent = this->write_iov_to_socket_(iov, iovcnt);
149 if (sent == static_cast<ssize_t>(total_write_len))
150 return APIError::OK;
151 // Partial write or -1: fall through to error check / enqueue below
152 } else {
153 // Overflow backlog remains after drain; skip socket write, enqueue everything
154 sent = 0;
155 }
156 }
157 // WRITE_FAILED (-1): fast path or retry write returned -1, check errno
158 if (sent == WRITE_FAILED) {
159 int err = errno;
160 if (err != EWOULDBLOCK && err != EAGAIN) {
161 this->state_ = State::FAILED;
162 HELPER_LOG("Socket write failed with errno %d", err);
164 }
165 sent = 0; // Treat WOULD_BLOCK as zero bytes sent
166 }
167 }
168
169 // Full write completed (possible when called directly, not via write_raw_fast_buf_)
170 if (sent == static_cast<ssize_t>(total_write_len))
171 return APIError::OK;
172
173 // Queue unsent data into overflow buffer
174 if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
175 HELPER_LOG("Overflow buffer full, dropping connection");
176 this->state_ = State::FAILED;
178 }
179 return APIError::OK;
180}
181
182const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
183 if (this->socket_) {
184 this->socket_->getpeername_to(buf);
185 } else {
186 buf[0] = '\0';
187 }
188 return buf.data();
189}
190
192 if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
193 HELPER_LOG("Bad state for init %d", (int) state_);
194 return APIError::BAD_STATE;
195 }
196 int err = this->socket_->setblocking(false);
197 if (err != 0) {
199 HELPER_LOG("Setting nonblocking failed with errno %d", errno);
201 }
202
203 int enable = 1;
204 err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
205 if (err != 0) {
207 HELPER_LOG("Setting nodelay failed with errno %d", errno);
209 }
210 return APIError::OK;
211}
212
214 if (received == -1) {
215 const int err = errno;
216 if (err == EWOULDBLOCK || err == EAGAIN) {
218 }
220 HELPER_LOG("Socket read failed with errno %d", err);
222 } else if (received == 0) {
224 HELPER_LOG("Connection closed");
226 }
227 return APIError::OK;
228}
229
230} // namespace esphome::api
231#endif
APIError handle_socket_read_result_(ssize_t received)
void log_packet_sending_(const void *data, uint16_t len)
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)
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_
ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt)
bool empty() const
True when no backlogged data is waiting.
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip)
Enqueue unsent IOV data into the backlog.
ssize_t try_drain(socket::Socket *socket)
Try to drain queued data to the socket.
__int64 ssize_t
Definition httplib.h:178
const LogString * api_error_to_logstr(APIError err)
const void size_t len
Definition hal.h:64