ESPHome 2026.3.0
Loading...
Searching...
No Matches
lwip_raw_tcp_impl.h
Go to the documentation of this file.
1#pragma once
3
4#ifdef USE_SOCKET_IMPL_LWIP_TCP
5
6#include <array>
7#include <cerrno>
8#include <cstring>
9#include <memory>
10#include <span>
11
13#include "headers.h"
14#include "lwip/ip.h"
15#include "lwip/netif.h"
16#include "lwip/opt.h"
17#include "lwip/tcp.h"
18
19namespace esphome::socket {
20
21// Forward declaration
22class LWIPRawImpl;
23
28 public:
29 LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {}
31 LWIPRawCommon(const LWIPRawCommon &) = delete;
33
34 int bind(const struct sockaddr *name, socklen_t addrlen);
35 int close();
36 int shutdown(int how);
37
38 int getpeername(struct sockaddr *name, socklen_t *addrlen);
39 int getsockname(struct sockaddr *name, socklen_t *addrlen);
40
42 size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
44 size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
45
46 int getsockopt(int level, int optname, void *optval, socklen_t *optlen);
47 int setsockopt(int level, int optname, const void *optval, socklen_t optlen);
48
49 int get_fd() const { return -1; }
50
51 protected:
52 int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);
53
54 // Member ordering optimized to minimize padding on 32-bit systems
55 struct tcp_pcb *pcb_;
56 // don't use lwip nodelay flag, it sometimes causes reconnect
57 // instead use it for determining whether to call lwip_output
58 bool nodelay_ = false;
60 uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
61};
62
65class LWIPRawImpl : public LWIPRawCommon {
66 public:
69
70 void init(struct pbuf *initial_rx = nullptr, bool initial_rx_closed = false);
71
72 // Non-listening sockets return error
73 std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) {
74 errno = EINVAL;
75 return nullptr;
76 }
77 std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
78 return this->accept(addr, addrlen);
79 }
80 // Regular sockets can't be converted to listening - this shouldn't happen
81 // as listen() should only be called on sockets created for listening
82 int listen(int) {
83 errno = EOPNOTSUPP;
84 return -1;
85 }
86 ssize_t read(void *buf, size_t len);
87 ssize_t readv(const struct iovec *iov, int iovcnt);
88 ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
89 errno = ENOTSUP;
90 return -1;
91 }
92 ssize_t write(const void *buf, size_t len);
93 ssize_t writev(const struct iovec *iov, int iovcnt);
94 ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
95 // return ::sendto(fd_, buf, len, flags, to, tolen);
96 errno = ENOSYS;
97 return -1;
98 }
99 // Intentionally unlocked — this is a polling check called every loop iteration.
100 // A stale read at worst delays processing by one loop tick; the actual I/O in
101 // read() holds the lwip lock and re-checks properly. See esphome#10681.
102 bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
103
104 // No lock needed — only called during setup before callbacks are registered.
105 // A stale pcb_ read is benign (returns ECONNRESET, which the caller handles).
106 int setblocking(bool blocking) {
107 if (this->pcb_ == nullptr) {
108 errno = ECONNRESET;
109 return -1;
110 }
111 // Raw TCP doesn't use a blocking flag directly. Blocking behavior
112 // is provided by SO_RCVTIMEO which makes read() wait via socket_delay().
113 return 0;
114 }
115 int loop() { return 0; }
116
117 err_t recv_fn(struct pbuf *pb, err_t err);
118
119 static void s_err_fn(void *arg, err_t err);
120 static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
121
122 protected:
123 // True when the socket could receive data but none has arrived yet.
124 // Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool,
125 // all atomic on ARM/Xtensa. A stale value is harmless: the caller either does
126 // an unnecessary wait (stale true) or skips it (stale false), and the
127 // authoritative recheck happens under LWIP_LOCK afterward.
128 bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; }
129 void wait_for_data_();
130 ssize_t read_locked_(void *buf, size_t len);
131 ssize_t internal_write_(const void *buf, size_t len);
132 int internal_output_();
133
134 pbuf *rx_buf_ = nullptr;
135 size_t rx_buf_offset_ = 0;
136 bool rx_closed_ = false;
137};
138
142 public:
145
146 void init();
147
148 // Intentionally unlocked — polling check, see LWIPRawImpl::ready() comment.
149 bool ready() const { return this->accepted_socket_count_ > 0; }
150
151 std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen);
152 std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
153 return this->accept(addr, addrlen);
154 }
155 int listen(int backlog);
156
157 // Listening sockets don't do I/O
158 ssize_t read(void *, size_t) {
159 errno = ENOTSUP;
160 return -1;
161 }
162 ssize_t write(const void *, size_t) {
163 errno = ENOTSUP;
164 return -1;
165 }
166 ssize_t readv(const struct iovec *, int) {
167 errno = ENOTSUP;
168 return -1;
169 }
170 ssize_t writev(const struct iovec *, int) {
171 errno = ENOTSUP;
172 return -1;
173 }
174 ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
175 errno = ENOTSUP;
176 return -1;
177 }
178 ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
179 errno = ENOTSUP;
180 return -1;
181 }
182 int setblocking(bool) { return 0; }
183 int loop() { return 0; }
184
185 static void s_err_fn(void *arg, err_t err);
186
187 private:
188 err_t accept_fn_(struct tcp_pcb *newpcb, err_t err);
189 static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err);
190
191 // Temporary callbacks for queued PCBs (between accept_fn_ and accept())
192 static void s_queued_err_fn(void *arg, err_t err);
193 static err_t s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
194
195 // Accept queue entry — stores a raw tcp_pcb and any data received while queued.
196 // lwip's default tcp_recv_null handler drops data and ACKs it, so we must register
197 // a temporary recv callback to buffer any data that arrives between accept_fn_
198 // (which stores the PCB) and accept() (which creates the LWIPRawImpl).
199 struct QueuedPcb {
200 struct tcp_pcb *pcb{nullptr};
201 struct pbuf *rx_buf{nullptr}; // Data received while queued (before accept() picks it up)
202 bool rx_closed{false}; // Remote sent FIN while queued
203 };
204
205 // Accept queue — stores raw tcp_pcb entries instead of heap-allocated LWIPRawImpl objects.
206 // LWIPRawImpl creation is deferred to the main-loop accept() call. This avoids:
207 // - Heap allocation in the accept callback (unsafe from IRQ context on RP2040)
208 // - Dangling LWIPRawImpl if the connection errors before accept() picks it up
209 // 2 slots is plenty since the main loop drains the queue every iteration.
210 static constexpr size_t MAX_ACCEPTED_SOCKETS = 2;
211 std::array<QueuedPcb, MAX_ACCEPTED_SOCKETS> accepted_pcbs_{};
212 uint8_t accepted_socket_count_ = 0; // Number of entries currently in queue
213};
214
215} // namespace esphome::socket
216
217#endif // USE_SOCKET_IMPL_LWIP_TCP
Non-virtual common base for LWIP raw TCP sockets.
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)
LWIPRawCommon(const LWIPRawCommon &)=delete
LWIPRawCommon & operator=(const LWIPRawCommon &)=delete
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)
int getpeername(struct sockaddr *name, socklen_t *addrlen)
LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb)
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)
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *)
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)
std::unique_ptr< LWIPRawImpl > accept(struct sockaddr *, socklen_t *)
ssize_t readv(const struct iovec *iov, int iovcnt)
static void s_err_fn(void *arg, err_t err)
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t)
std::unique_ptr< LWIPRawImpl > accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen)
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)
ssize_t writev(const struct iovec *iov, int iovcnt)
Listening socket implementation for LWIP raw TCP.
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *)
static void s_err_fn(void *arg, err_t err)
std::unique_ptr< LWIPRawImpl > accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen)
ssize_t writev(const struct iovec *, int)
std::unique_ptr< LWIPRawImpl > accept(struct sockaddr *addr, socklen_t *addrlen)
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t)
ssize_t readv(const struct iovec *, int)
ssize_t write(const void *, size_t)
uint32_t socklen_t
Definition headers.h:99
uint8_t sa_family_t
Definition headers.h:59
__int64 ssize_t
Definition httplib.h:178
in_addr ip_addr_t
Definition ip_address.h:22
std::string size_t len
Definition helpers.h:892