ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
lwip_raw_tcp_impl.cpp
Go to the documentation of this file.
1#include "socket.h"
3
4#ifdef USE_SOCKET_IMPL_LWIP_TCP
5
6#include <cerrno>
7#include <cstring>
8#include <sys/time.h>
9
11#include "esphome/core/wake.h"
12#include "esphome/core/log.h"
14
15#ifdef USE_OTA_PLATFORM_ESPHOME
17#endif
18
19#ifdef USE_ESP8266
20#include <coredecls.h> // For esp_schedule()
21#elif defined(USE_RP2)
22#include <hardware/sync.h> // For __sev(), __wfe()
23#include <pico/time.h> // For add_alarm_in_ms(), cancel_alarm()
24#endif
25
26namespace esphome::socket {
27
28// LWIP thread safety — see lwip_raw_common_impl.h for full explanation.
29// esphome::LwIPLock is the platform-provided RAII guard.
30// On RP2040, it acquires cyw43_arch_lwip_begin/end. On ESP8266, it's a no-op.
31#define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT
32
33ESPHOME_LOG_TAG(TAG, "socket");
34
35#ifdef USE_ESP8266
36// optimistic_yield() rate limit in microseconds of CONT time; cheap when hot.
37static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
38// Let SYS run so queued WiFi traffic reaches lwip; CONT and SYS are cooperative
39static inline void yield_to_sys() { optimistic_yield(ESP8266_YIELD_INTERVAL_US); }
40#else
41static inline void yield_to_sys() {}
42#endif
43
44// set to 1 to enable verbose lwip logging
45#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
46#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
47#else
48#define LWIP_LOG(msg, ...)
49#endif
50
51// Clear arg, recv, and err callbacks, then abort a connected PCB.
52// Only valid for full tcp_pcb (not tcp_pcb_listen).
53// Must be called before destroying the object that tcp_arg points to —
54// tcp_abort() triggers the err callback synchronously, which would
55// otherwise call back into a partially-destroyed object.
56// tcp_sent/tcp_poll are never registered and the connect callback cannot
57// fire after abort or close, so neither is cleared.
58static void pcb_detach_abort(struct tcp_pcb *pcb) {
59 tcp_arg(pcb, nullptr);
60 tcp_recv(pcb, nullptr);
61 tcp_err(pcb, nullptr);
62 tcp_abort(pcb);
63}
64
65// Clear arg, recv, and err callbacks, then gracefully close a connected PCB.
66// Only valid for full tcp_pcb (not tcp_pcb_listen).
67// After tcp_close(), the PCB remains alive during the TCP close handshake
68// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP
69// would call recv/err on a destroyed socket object, corrupting the heap.
70// Callbacks are left as in pcb_detach_abort().
71// Returns ERR_OK on success; on failure the PCB is aborted instead.
72static err_t pcb_detach_close(struct tcp_pcb *pcb) {
73 tcp_arg(pcb, nullptr);
74 tcp_recv(pcb, nullptr);
75 tcp_err(pcb, nullptr);
76 err_t err = tcp_close(pcb);
77 if (err != ERR_OK) {
78 tcp_abort(pcb);
79 }
80 return err;
81}
82
83// ---- LWIPRawCommon methods ----
84
86 LWIP_LOCK();
87 if (this->pcb_ != nullptr) {
88 LWIP_LOG("tcp_abort(%p)", this->pcb_);
89 pcb_detach_abort(this->pcb_);
90 this->pcb_ = nullptr;
91 }
92}
93
94bool LWIPRawCommon::sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port,
95 bool for_bind) const {
96 if (name != nullptr && name->sa_family == this->family_ &&
97 (for_bind ? sockaddr_to_lwip_bind(this->family_, name, addrlen, ip, port)
98 : sockaddr_to_lwip(name, addrlen, ip, port)))
99 return true;
100 errno = EINVAL;
101 return false;
102}
103
104int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) {
105 LWIP_LOCK();
106 if (this->pcb_ == nullptr) {
107 errno = EBADF;
108 return -1;
109 }
110 ip_addr_t ip;
111 uint16_t port;
112 if (!this->sockaddr2ip_(name, addrlen, &ip, &port, true)) {
113 return -1;
114 }
115 LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
116 err_t err = tcp_bind(this->pcb_, &ip, port);
117 if (err != ERR_OK) {
118 LWIP_LOG(" -> err %d", err);
119 errno = lwip_err_to_errno(err);
120 return -1;
121 }
122 return 0;
123}
124
126 LWIP_LOCK();
127 if (this->pcb_ == nullptr) {
128 errno = ECONNRESET;
129 return -1;
130 }
131 LWIP_LOG("tcp_close(%p)", this->pcb_);
132 err_t err = pcb_detach_close(this->pcb_);
133 this->pcb_ = nullptr;
134 if (err != ERR_OK) {
135 LWIP_LOG(" -> err %d", err);
136 errno = lwip_err_to_errno(err);
137 return -1;
138 }
139 return 0;
140}
141
143 LWIP_LOCK();
144 if (this->pcb_ == nullptr) {
145 errno = ECONNRESET;
146 return -1;
147 }
148 bool shut_rx = false, shut_tx = false;
149 if (how == SHUT_RD) {
150 shut_rx = true;
151 } else if (how == SHUT_WR) {
152 shut_tx = true;
153 } else if (how == SHUT_RDWR) {
154 shut_rx = shut_tx = true;
155 } else {
156 errno = EINVAL;
157 return -1;
158 }
159 LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", this->pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0);
160 err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx);
161 if (err != ERR_OK) {
162 LWIP_LOG(" -> err %d", err);
163 errno = lwip_err_to_errno(err);
164 return -1;
165 }
166 return 0;
167}
168
169int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) {
170 LWIP_LOCK();
171 if (this->pcb_ == nullptr) {
172 errno = ECONNRESET;
173 return -1;
174 }
175 if (name == nullptr || addrlen == nullptr) {
176 errno = EINVAL;
177 return -1;
178 }
179 return this->ip2sockaddr_(&this->pcb_->remote_ip, this->pcb_->remote_port, name, addrlen);
180}
181
182int LWIPRawCommon::getsockname(struct sockaddr *name, socklen_t *addrlen) {
183 LWIP_LOCK();
184 if (this->pcb_ == nullptr) {
185 errno = ECONNRESET;
186 return -1;
187 }
188 if (name == nullptr || addrlen == nullptr) {
189 errno = EINVAL;
190 return -1;
191 }
192 return this->ip2sockaddr_(&this->pcb_->local_ip, this->pcb_->local_port, name, addrlen);
193}
194
195size_t LWIPRawCommon::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
196 struct sockaddr_storage storage;
197 socklen_t len = sizeof(storage);
198 if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
199 buf[0] = '\0';
200 return 0;
201 }
202 return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
203}
204
205size_t LWIPRawCommon::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
206 struct sockaddr_storage storage;
207 socklen_t len = sizeof(storage);
208 if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
209 buf[0] = '\0';
210 return 0;
211 }
212 return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
213}
214
215int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
216 LWIP_LOCK();
217 if (this->pcb_ == nullptr) {
218 errno = ECONNRESET;
219 return -1;
220 }
221 if (optlen == nullptr || optval == nullptr) {
222 errno = EINVAL;
223 return -1;
224 }
225 if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
226 if (*optlen < 4) {
227 errno = EINVAL;
228 return -1;
229 }
230 // lwip doesn't seem to have this feature. Don't send an error
231 // to prevent warnings
232 *reinterpret_cast<int *>(optval) = 1;
233 *optlen = 4;
234 return 0;
235 }
236 if (level == SOL_SOCKET && optname == SO_RCVTIMEO) {
237 if (*optlen < sizeof(struct timeval)) {
238 errno = EINVAL;
239 return -1;
240 }
241 uint32_t ms = this->recv_timeout_cs_ * 10;
242 auto *tv = reinterpret_cast<struct timeval *>(optval);
243 tv->tv_sec = ms / 1000;
244 tv->tv_usec = (ms % 1000) * 1000;
245 *optlen = sizeof(struct timeval);
246 return 0;
247 }
248 if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
249 if (*optlen < 4) {
250 errno = EINVAL;
251 return -1;
252 }
253 *reinterpret_cast<int *>(optval) = this->nodelay_;
254 *optlen = 4;
255 return 0;
256 }
257
258 errno = EINVAL;
259 return -1;
260}
261
262int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
263 LWIP_LOCK();
264 if (this->pcb_ == nullptr) {
265 errno = ECONNRESET;
266 return -1;
267 }
268 if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
269 if (optlen != 4) {
270 errno = EINVAL;
271 return -1;
272 }
273 // lwip doesn't seem to have this feature. Don't send an error
274 // to prevent warnings
275 return 0;
276 }
277 if (level == SOL_SOCKET && optname == SO_RCVTIMEO) {
278 if (optlen < sizeof(struct timeval)) {
279 errno = EINVAL;
280 return -1;
281 }
282 const auto *tv = reinterpret_cast<const struct timeval *>(optval);
283 uint32_t ms = tv->tv_sec * 1000 + tv->tv_usec / 1000;
284 uint32_t cs = (ms + 9) / 10; // round up to nearest centisecond
285 this->recv_timeout_cs_ = cs > 255 ? 255 : static_cast<uint8_t>(cs);
286 return 0;
287 }
288 if (level == SOL_SOCKET && optname == SO_SNDTIMEO) {
289 // Raw TCP writes are non-blocking (tcp_write), so send timeout is a no-op.
290 return 0;
291 }
292 if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
293 if (optlen != 4) {
294 errno = EINVAL;
295 return -1;
296 }
297 int val = *reinterpret_cast<const int *>(optval);
298 this->nodelay_ = val;
299 return 0;
300 }
301
302 errno = EINVAL;
303 return -1;
304}
305
306int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) {
307 // lwip pcb ports are host order; ntohs preserves historical byte-swapped sin_port output
308 return lwip_ip_to_sockaddr(this->family_, ip, ntohs(port), name, addrlen);
309}
310
311// ---- LWIPRawImpl methods ----
312
314 LWIP_LOCK();
315 // Free any received pbufs that LWIP transferred ownership of via recv_fn.
316 // tcp_abort() in the base destructor won't free these since LWIP considers
317 // ownership transferred once the recv callback accepts them.
318 if (this->rx_buf_ != nullptr) {
319 pbuf_free(this->rx_buf_);
320 this->rx_buf_ = nullptr;
321 }
322 // Base class destructor handles pcb_ cleanup via tcp_abort
323}
324
325void LWIPRawImpl::init(struct pbuf *initial_rx, bool initial_rx_closed) {
326 LWIP_LOCK();
327 LWIP_LOG("init(%p)", this->pcb_);
328 tcp_arg(this->pcb_, this);
329 tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn);
330 tcp_err(this->pcb_, LWIPRawImpl::s_err_fn);
331 if (initial_rx != nullptr) {
332 this->rx_buf_ = initial_rx;
333 this->rx_buf_offset_ = 0;
334 }
335 this->rx_closed_ = initial_rx_closed;
336}
337
338void LWIPRawImpl::s_err_fn(void *arg, err_t err) {
339 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
340 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
341 // No LWIP_LOCK() needed — lwip core already holds the async_context lock.
342 //
343 // pcb is already freed when this callback is called
344 // ERR_RST: connection was reset by remote host
345 // ERR_ABRT: aborted through tcp_abort or TCP timer
346 auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
347 ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err);
348 if (arg_this->connect_err_ == EINPROGRESS) {
349 // Refused (RST) or SYN retries exhausted; written before pcb_ so
350 // poll_connect() never sees a dead pcb without its reason
351 arg_this->connect_err_ = err == ERR_RST ? ECONNREFUSED : ETIMEDOUT;
352 }
353 arg_this->pcb_ = nullptr;
355}
356
357err_t LWIPRawImpl::s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err) {
358 // LWIP CALLBACK, same constraints as s_err_fn; err is always ERR_OK
359 auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
360 arg_this->connect_err_ = EISCONN;
362 return ERR_OK;
363}
364
365int LWIPRawImpl::connect(const struct sockaddr *addr, socklen_t addrlen) {
366 LWIP_LOCK();
367 if (this->pcb_ == nullptr) {
368 errno = EBADF;
369 return -1;
370 }
371 if (this->connect_err_ == EINPROGRESS || this->connect_err_ == EISCONN) {
372 errno = this->connect_err_ == EINPROGRESS ? EALREADY : EISCONN;
373 return -1;
374 }
375 ip_addr_t ip;
376 uint16_t port;
377 // Concrete type for tcp_connect; a remembered IPv4 peer arrives v4-mapped and is unmapped
378 if (!this->sockaddr2ip_(addr, addrlen, &ip, &port, false)) {
379 return -1;
380 }
381 LWIP_LOG("tcp_connect(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
382 err_t err = tcp_connect(this->pcb_, &ip, port, LWIPRawImpl::s_connected_fn);
383 if (err != ERR_OK) {
384 LWIP_LOG(" -> err %d", err);
385 errno = lwip_err_to_errno(err);
386 return -1;
387 }
388 this->connect_err_ = EINPROGRESS;
389 errno = EINPROGRESS;
390 return -1;
391}
392
394 // pcb_ first; see the ordering note on the declaration
395 if (this->pcb_ == nullptr) {
396 // Only a recorded connect failure carries its own reason
397 const bool failed = this->connect_err_ == ECONNREFUSED || this->connect_err_ == ETIMEDOUT;
398 err_out = failed ? this->connect_err_ : ECONNRESET;
400 }
401 switch (this->connect_err_) {
402 case EINPROGRESS:
403 yield_to_sys(); // so the SYN-ACK is processed between polls
405 case EISCONN:
407 case 0:
408 err_out = EINVAL; // no connect was started
410 default:
411 err_out = this->connect_err_;
413 }
414}
415
416err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
417 auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
418 return arg_this->recv_fn(pb, err);
419}
420
421err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) {
422 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
423 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
424 LWIP_LOG("recv(pb=%p err=%d)", pb, err);
425 if (err != 0) {
426 // "An error code if there has been an error receiving Only return ERR_ABRT if you have
427 // called tcp_abort from within the callback function!"
428 if (pb != nullptr) {
429 pbuf_free(pb);
430 }
431 this->rx_closed_ = true;
432 return ERR_OK;
433 }
434 if (pb == nullptr) {
435 this->rx_closed_ = true;
436 return ERR_OK;
437 }
438 if (this->rx_buf_ == nullptr) {
439 // no need to copy because lwIP gave control of it to us
440 this->rx_buf_ = pb;
441 this->rx_buf_offset_ = 0;
442 } else {
443 pbuf_cat(this->rx_buf_, pb);
444 }
445 // Wake the main loop immediately so it can process the received data.
447 return ERR_OK;
448}
449
451 // Wait for data without holding LWIP_LOCK so recv_fn() can run on RP2040
452 // (needs async_context lock).
453 //
454 // Loop until data arrives, connection closes, or the full timeout elapses.
455 // wakeable_delay() may return early due to any wake source,
456 // so we re-enter for the remaining time.
457 uint32_t timeout_ms = this->recv_timeout_cs_ * 10;
458 uint32_t start = millis();
459 while (this->waiting_for_data_()) {
460 uint32_t elapsed = millis() - start;
461 if (elapsed >= timeout_ms)
462 break;
463 esphome::internal::wakeable_delay(timeout_ms - elapsed);
464 }
465}
466
468 // Caller must hold LWIP_LOCK. Copies available data from rx_buf_ into buf.
469 if (this->pcb_ == nullptr) {
470 errno = ECONNRESET;
471 return -1;
472 }
473 if (this->rx_closed_ && this->rx_buf_ == nullptr) {
474 return 0;
475 }
476 if (len == 0) {
477 return 0;
478 }
479 if (this->rx_buf_ == nullptr) {
480 errno = EWOULDBLOCK;
481 return -1;
482 }
483
484 size_t read = 0;
485 uint8_t *buf8 = reinterpret_cast<uint8_t *>(buf);
486 while (len && this->rx_buf_ != nullptr) {
487 size_t pb_len = this->rx_buf_->len;
488 size_t pb_left = pb_len - this->rx_buf_offset_;
489 if (pb_left == 0)
490 break;
491 size_t copysize = std::min(len, pb_left);
492 memcpy(buf8, reinterpret_cast<uint8_t *>(this->rx_buf_->payload) + this->rx_buf_offset_, copysize);
493
494 if (pb_left == copysize) {
495 // full pb copied, free it
496 if (this->rx_buf_->next == nullptr) {
497 // last buffer in chain
498 pbuf_free(this->rx_buf_);
499 this->rx_buf_ = nullptr;
500 this->rx_buf_offset_ = 0;
501 } else {
502 auto *old_buf = this->rx_buf_;
503 this->rx_buf_ = this->rx_buf_->next;
504 pbuf_ref(this->rx_buf_);
505 pbuf_free(old_buf);
506 this->rx_buf_offset_ = 0;
507 }
508 } else {
509 this->rx_buf_offset_ += copysize;
510 }
511 LWIP_LOG("tcp_recved(%p %u)", this->pcb_, copysize);
512 tcp_recved(this->pcb_, copysize);
513
514 buf8 += copysize;
515 len -= copysize;
516 read += copysize;
517 }
518
519 if (read == 0) {
520 errno = EWOULDBLOCK;
521 return -1;
522 }
523
524 return read;
525}
526
527ssize_t LWIPRawImpl::read(void *buf, size_t len) {
528 // Let queued WiFi RX reach lwip first; otherwise inbound segments can
529 // sit unprocessed for seconds while the main loop polls
530 if (this->waiting_for_data_()) {
531 yield_to_sys();
532 }
533 // See waiting_for_data_() for safety of unlocked reads.
534 if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
535 this->wait_for_data_();
536 }
537
538 LWIP_LOCK();
539 return this->read_locked_(buf, len);
540}
541
542ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) {
543 // No ESP8266 SYS yield here: only read() needs it today. If a consumer
544 // switches to scatter-gather reads, mirror the yield from read().
545 // See waiting_for_data_() for safety of unlocked reads.
546 if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
547 this->wait_for_data_();
548 }
549
550 LWIP_LOCK(); // Hold for entire scatter-gather operation
551 ssize_t ret = 0;
552 for (int i = 0; i < iovcnt; i++) {
553 ssize_t err = this->read_locked_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
554 if (err == -1) {
555 if (ret != 0) {
556 // if we already read some don't return an error
557 break;
558 }
559 return err;
560 }
561 ret += err;
562 if ((size_t) err != iov[i].iov_len)
563 break;
564 }
565 return ret;
566}
567
568ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) {
569 LWIP_LOCK();
570 if (this->pcb_ == nullptr) {
571 errno = ECONNRESET;
572 return -1;
573 }
574 if (len == 0)
575 return 0;
576 if (buf == nullptr) {
577 errno = EINVAL;
578 return 0;
579 }
580 auto space = tcp_sndbuf(this->pcb_);
581 if (space == 0) {
582 errno = EWOULDBLOCK;
583 return -1;
584 }
585 size_t to_send = std::min((size_t) space, len);
586 LWIP_LOG("tcp_write(%p buf=%p %u)", this->pcb_, buf, to_send);
587 err_t err = tcp_write(this->pcb_, buf, to_send, TCP_WRITE_FLAG_COPY);
588 if (err == ERR_MEM) {
589 LWIP_LOG(" -> err ERR_MEM");
590 errno = EWOULDBLOCK;
591 return -1;
592 }
593 if (err != ERR_OK) {
594 LWIP_LOG(" -> err %d", err);
595 errno = ECONNRESET;
596 return -1;
597 }
598 return to_send;
599}
600
602 LWIP_LOCK();
603 if (this->pcb_ == nullptr) {
604 errno = ECONNRESET;
605 return -1;
606 }
607 LWIP_LOG("tcp_output(%p)", this->pcb_);
608 err_t err = tcp_output(this->pcb_);
609 if (err != ERR_OK) {
610 LWIP_LOG(" -> err %d", err);
611 // ERR_ABRT: sometimes lwip returns it for no apparent reason; the
612 // connection works fine afterwards, and back with ESPAsyncTCP we
613 // indirectly also ignored this error, so treat it as success for
614 // flush purposes too.
615 // FIXME: figure out where this is returned and what it means in this context
616 if (err != ERR_ABRT) {
617 errno = ECONNRESET;
618 return -1;
619 }
620 }
621 // Flushed: yield to SYS so the queued segments reach the WiFi driver
622 // instead of waiting seconds for an unrelated SYS slot. Callers only get
623 // here after a successful tcp_write, so idle paths never yield.
624 yield_to_sys();
625 return 0;
626}
627
628ssize_t LWIPRawImpl::write(const void *buf, size_t len) {
629 LWIP_LOCK(); // Hold for write + optional output
630 ssize_t written = this->internal_write_(buf, len);
631 if (written == -1)
632 return -1;
633 if (written == 0) {
634 // no need to output if nothing written
635 return 0;
636 }
637 if (this->nodelay_) {
638 int err = this->internal_output_();
639 if (err == -1)
640 return -1;
641 }
642 return written;
643}
644
645ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) {
646 LWIP_LOCK(); // Hold for entire scatter-gather operation
647 ssize_t written = 0;
648 for (int i = 0; i < iovcnt; i++) {
649 ssize_t err = this->internal_write_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
650 if (err == -1) {
651 if (written != 0) {
652 // if we already read some don't return an error
653 break;
654 }
655 return err;
656 }
657 written += err;
658 if ((size_t) err != iov[i].iov_len)
659 break;
660 }
661 if (written == 0) {
662 // no need to output if nothing written
663 return 0;
664 }
665 if (this->nodelay_) {
666 int err = this->internal_output_();
667 if (err == -1)
668 return -1;
669 }
670 return written;
671}
672
673// ---- LWIPRawListenImpl methods ----
674
676 LWIP_LOCK();
677 // Abort any queued PCBs that were never accepted by the main loop.
678 for (uint8_t i = 0; i < this->accepted_socket_count_; i++) {
679 auto &entry = this->accepted_pcbs_[i];
680 if (entry.pcb != nullptr) {
681 pcb_detach_abort(entry.pcb);
682 entry.pcb = nullptr;
683 }
684 if (entry.rx_buf != nullptr) {
685 pbuf_free(entry.rx_buf);
686 entry.rx_buf = nullptr;
687 }
688 }
689 this->accepted_socket_count_ = 0;
690 // Listen PCBs must use tcp_close(), not tcp_abort().
691 // tcp_abandon() asserts pcb->state != LISTEN and would access
692 // fields that don't exist in the smaller tcp_pcb_listen struct.
693 // Don't use pcb_detach_close() here — tcp_recv()/tcp_err() also access
694 // fields that only exist in the full tcp_pcb, not tcp_pcb_listen.
695 // tcp_close() on a listen PCB is synchronous (frees immediately),
696 // so there are no async callbacks to worry about.
697 // Close here and null pcb_ so the base destructor skips tcp_abort.
698 if (this->pcb_ != nullptr) {
699 tcp_close(this->pcb_);
700 this->pcb_ = nullptr;
701 }
702}
703
705 LWIP_LOCK();
706 LWIP_LOG("init(%p)", this->pcb_);
707 tcp_arg(this->pcb_, this);
708 tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn);
709 tcp_err(this->pcb_, LWIPRawListenImpl::s_err_fn);
710}
711
712void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) {
713 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
714 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
715 auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg);
716 ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err);
717 arg_this->pcb_ = nullptr;
718}
719
720void LWIPRawListenImpl::s_queued_err_fn(void *arg, err_t err) {
721 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
722 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
723 // Called when a queued (not yet accepted) PCB errors — e.g., remote sent RST.
724 // The PCB is already freed by lwip. Null our pointer so accept() skips it.
725 (void) err;
726 auto *entry = reinterpret_cast<QueuedPcb *>(arg);
727 entry->pcb = nullptr;
728 // Don't free rx_buf here — accept() will clean it up when it sees pcb==nullptr
729}
730
731err_t LWIPRawListenImpl::s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
732 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
733 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
734 // Temporary recv callback for PCBs queued between accept_fn_ and accept().
735 // Without this, lwip's default tcp_recv_null handler would ACK and drop the data,
736 // causing the API handshake to silently fail (client sends Hello, server never sees it).
737 (void) pcb;
738 auto *entry = reinterpret_cast<QueuedPcb *>(arg);
739 if (pb == nullptr || err != ERR_OK) {
740 // Remote closed or error
741 if (pb != nullptr) {
742 pbuf_free(pb);
743 }
744 entry->rx_closed = true;
745 return ERR_OK;
746 }
747 // Buffer the data — tcp_recved() is deferred to read() after accept() creates the socket.
748 if (entry->rx_buf == nullptr) {
749 entry->rx_buf = pb;
750 } else {
751 pbuf_cat(entry->rx_buf, pb);
752 }
753 return ERR_OK;
754}
755
756err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
757 auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg);
758 return arg_this->accept_fn_(newpcb, err);
759}
760
761std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, socklen_t *addrlen) {
762 LWIP_LOCK();
763 if (this->pcb_ == nullptr) {
764 errno = EBADF;
765 return nullptr;
766 }
767 // Dequeue front entry, skipping any null entries (PCBs freed by lwip while queued).
768 // The error callback nulled their pcb pointers; clean up buffered data and discard.
769 while (this->accepted_socket_count_ > 0) {
770 QueuedPcb entry = this->accepted_pcbs_[0];
771 // Shift remaining entries forward, updating tcp_arg pointers as we go.
772 // Safe because we hold LWIP_LOCK, so err/recv callbacks can't fire during the update.
773 for (uint8_t i = 1; i < this->accepted_socket_count_; i++) {
774 this->accepted_pcbs_[i - 1] = this->accepted_pcbs_[i];
775 if (this->accepted_pcbs_[i - 1].pcb != nullptr) {
776 tcp_arg(this->accepted_pcbs_[i - 1].pcb, &this->accepted_pcbs_[i - 1]);
777 }
778 }
779 this->accepted_pcbs_[this->accepted_socket_count_ - 1] = {};
780 this->accepted_socket_count_--;
781 if (entry.pcb == nullptr) {
782 // PCB was freed by lwip (RST/timeout) while queued — discard and try next
783 if (entry.rx_buf != nullptr) {
784 pbuf_free(entry.rx_buf);
785 }
786 continue;
787 }
788 LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_);
789 // Create socket wrapper on the main loop (not in accept callback) to avoid
790 // heap allocation in IRQ context on RP2040. Transfer any data received while queued.
791 auto sock = make_unique<LWIPRawImpl>(this->family_, entry.pcb);
792 sock->init(entry.rx_buf, entry.rx_closed);
793 if (addr != nullptr) {
794 sock->getpeername(addr, addrlen);
795 }
796 LWIP_LOG("accept(%p)", sock.get());
797 return sock;
798 }
799 errno = EWOULDBLOCK;
800 return nullptr;
801}
802
804 LWIP_LOCK();
805 if (this->pcb_ == nullptr) {
806 errno = EBADF;
807 return -1;
808 }
809 LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", this->pcb_, backlog);
810 struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(this->pcb_, backlog);
811 if (listen_pcb == nullptr) {
812 tcp_abort(this->pcb_);
813 this->pcb_ = nullptr;
814 errno = EOPNOTSUPP;
815 return -1;
816 }
817 // tcp_listen reallocates the pcb, replace ours
818 this->pcb_ = listen_pcb;
819 // set callbacks on new pcb
820 LWIP_LOG("tcp_arg(%p)", this->pcb_);
821 tcp_arg(this->pcb_, this);
822 tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn);
823 // Note: tcp_err() is NOT re-registered here. tcp_listen_with_backlog() converts the
824 // full tcp_pcb to a smaller tcp_pcb_listen struct that lacks the errf field.
825 // Calling tcp_err() on a listen PCB writes past the struct boundary (undefined behavior).
826 return 0;
827}
828
829err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) {
830 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
831 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
832 LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
833 if (err != ERR_OK || newpcb == nullptr) {
834 // "An error code if there has been an error accepting. Only return ERR_ABRT if you have
835 // called tcp_abort from within the callback function!"
836 // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d
837 // nothing to do here, we just don't push it to the queue
838 return ERR_OK;
839 }
840 // Check if we've reached the maximum accept queue size
841 if (this->accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) {
842 LWIP_LOG("Rejecting connection, queue full (%d)", this->accepted_socket_count_);
843 // Abort the connection when queue is full
844 tcp_abort(newpcb);
845 // Must return ERR_ABRT since we called tcp_abort()
846 return ERR_ABRT;
847 }
848 // Store the raw PCB — LWIPRawImpl creation is deferred to the main-loop accept().
849 // This avoids heap allocation in this callback, which is unsafe from IRQ context on RP2040.
850 uint8_t idx = this->accepted_socket_count_++;
851 this->accepted_pcbs_[idx] = {newpcb, nullptr, false};
852 // Register temporary callbacks so that while the PCB is queued:
853 // - err: nulls our pointer if the connection errors (RST, timeout)
854 // - recv: buffers any data that arrives before accept() creates the LWIPRawImpl
855 // (without this, lwip's default tcp_recv_null would ACK and drop the data)
856 // tcp_arg points to our queue entry; accept() updates these pointers after shifting.
857 tcp_arg(newpcb, &this->accepted_pcbs_[idx]);
858 tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn);
859 tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn);
860 LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
861#ifdef USE_OTA_PLATFORM_ESPHOME
862 // Must run before wake_loop_any_context() so flags are visible when the main task wakes.
864#endif
865 // Wake the main loop immediately so it can accept the new connection.
867 return ERR_OK;
868}
869
870// ---- TCP Factory functions ----
871
872std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
873 if (type != SOCK_STREAM) {
874 ESP_LOGE(TAG, "Use socket_udp() for UDP sockets on this platform");
875 errno = EPROTOTYPE;
876 return nullptr;
877 }
878 LWIP_LOCK();
879 auto *pcb = tcp_new();
880 if (pcb == nullptr)
881 return nullptr;
882 auto *sock = new LWIPRawImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory)
883 sock->init();
884 return std::unique_ptr<Socket>{sock};
885}
886
887std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol) {
888 // LWIPRawImpl doesn't use file descriptors, so monitoring is not applicable
889 return socket(domain, type, protocol);
890}
891
892std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
893 if (type != SOCK_STREAM) {
894 ESP_LOGE(TAG, "Use socket_udp() for UDP sockets on this platform");
895 errno = EPROTOTYPE;
896 return nullptr;
897 }
898 LWIP_LOCK();
899 auto *pcb = tcp_new();
900 if (pcb == nullptr)
901 return nullptr;
902 auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory)
903 sock->init();
904 return std::unique_ptr<ListenSocket>{sock};
905}
906
907std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
908 // LWIPRawImpl doesn't use file descriptors, so monitoring is not applicable
909 return socket_listen(domain, type, protocol);
910}
911
912#undef LWIP_LOCK
913
914} // namespace esphome::socket
915
916#endif // USE_SOCKET_IMPL_LWIP_TCP
int getsockname(struct sockaddr *name, socklen_t *addrlen)
size_t getsockname_to(std::span< char, SOCKADDR_STR_LEN > buf)
Format local address into a fixed-size buffer (no heap allocation)
int bind(const struct sockaddr *name, socklen_t addrlen)
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen)
int setsockopt(int level, int optname, const void *optval, socklen_t optlen)
int getsockopt(int level, int optname, void *optval, socklen_t *optlen)
bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port, bool for_bind) const
sockaddr of this socket's family to lwip address and port; false with errno on mismatch.
int getpeername(struct sockaddr *name, socklen_t *addrlen)
size_t getpeername_to(std::span< char, SOCKADDR_STR_LEN > buf)
Format peer address into a fixed-size buffer (no heap allocation)
Connected socket implementation for LWIP raw TCP.
ssize_t read_locked_(void *buf, size_t len)
ConnectPollResult poll_connect(int &err_out) const
int connect(const struct sockaddr *addr, socklen_t addrlen)
Non-blocking: returns -1/EINPROGRESS once the SYN is queued, see poll_connect().
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err)
void init(struct pbuf *initial_rx=nullptr, bool initial_rx_closed=false)
ssize_t readv(const struct iovec *iov, int iovcnt)
static void s_err_fn(void *arg, err_t err)
err_t recv_fn(struct pbuf *pb, err_t err)
ssize_t internal_write_(const void *buf, size_t len)
ssize_t write(const void *buf, size_t len)
ssize_t read(void *buf, size_t len)
static err_t s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err)
ssize_t writev(const struct iovec *iov, int iovcnt)
Listening socket implementation for LWIP raw TCP.
static void s_err_fn(void *arg, err_t err)
std::unique_ptr< LWIPRawImpl > accept(struct sockaddr *addr, socklen_t *addrlen)
uint8_t type
uint32_t socklen_t
Definition headers.h:109
uint8_t sa_family_t
Definition headers.h:69
__int64 ssize_t
Definition httplib.h:178
int ret
void esphome_wake_ota_component_any_context()
mopeka_std_values val[3]
void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms)
Host wakeable_delay uses select() over the registered fds — defined in wake_host.cpp.
bool sockaddr_to_lwip(const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip, uint16_t *port)
Convert sockaddr to lwip ip_addr_t and host-order port.
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span< char, SOCKADDR_STR_LEN > buf)
Format sockaddr into caller-provided buffer, returns length written (excluding null)
std::unique_ptr< ListenSocket > socket_listen(int domain, int type, int protocol)
Create a listening socket of the given domain, type and protocol.
ConnectPollResult
Outcome of polling a non-blocking connect(); see socket::poll_connect().
Definition headers.h:224
std::unique_ptr< ListenSocket > socket_listen_loop_monitored(int domain, int type, int protocol)
int lwip_err_to_errno(err_t err)
errno for a failed lwip call
ESPHOME_LOG_TAG(TAG, "socket")
bool sockaddr_to_lwip_bind(sa_family_t family, const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip, uint16_t *port)
sockaddr_to_lwip variant for bind: promotes AF_INET6 sockets to IPADDR_TYPE_ANY so they accept both I...
std::unique_ptr< Socket > socket(int domain, int type, int protocol)
Create a socket of the given domain, type and protocol.
std::unique_ptr< Socket > socket_loop_monitored(int domain, int type, int protocol)
Create a socket and monitor it for data in the main loop.
int lwip_ip_to_sockaddr(sa_family_t family, const ip_addr_t *ip, uint16_t port_host, struct sockaddr *name, socklen_t *addrlen)
Convert lwip ip_addr_t + host-order port to sockaddr, based on the socket's address family.
void esphome_wake_ota_component_any_context()
const void size_t len
Definition hal.h:68
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
int written
Definition helpers.h:1262
void IRAM_ATTR wake_loop_any_context()
IRAM_ATTR entry point for ISR callers — defined in wake_esp8266.cpp.
static void uint32_t
sa_family_t sa_family
Definition headers.h:97
Platform-specific main loop wake primitives.