ESPHome 2026.5.0-dev
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 // Check if the socket has buffered data ready to read.
100 // See the ready() contract in socket.h — callers must drain or track remaining data.
101 // Intentionally unlocked — this is a polling check called every loop iteration.
102 // A stale read at worst delays processing by one loop tick; the actual I/O in
103 // read() holds the lwip lock and re-checks properly. See esphome#10681.
104 bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
105
106 // No lock needed — only called during setup before callbacks are registered.
107 // A stale pcb_ read is benign (returns ECONNRESET, which the caller handles).
108 int setblocking(bool blocking) {
109 if (this->pcb_ == nullptr) {
110 errno = ECONNRESET;
111 return -1;
112 }
113 // Raw TCP doesn't use a blocking flag directly. Blocking behavior
114 // is provided by SO_RCVTIMEO which makes read() wait via wakeable_delay().
115 return 0;
116 }
117 int loop() { return 0; }
118
119 err_t recv_fn(struct pbuf *pb, err_t err);
120
121 static void s_err_fn(void *arg, err_t err);
122 static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
123
124 protected:
125 // True when the socket could receive data but none has arrived yet.
126 // Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool,
127 // all atomic on ARM/Xtensa. A stale value is harmless: the caller either does
128 // an unnecessary wait (stale true) or skips it (stale false), and the
129 // authoritative recheck happens under LWIP_LOCK afterward.
130 bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; }
131 void wait_for_data_();
132 ssize_t read_locked_(void *buf, size_t len);
133 ssize_t internal_write_(const void *buf, size_t len);
134 int internal_output_();
135
136 pbuf *rx_buf_ = nullptr;
137 size_t rx_buf_offset_ = 0;
138 bool rx_closed_ = false;
139};
140
144 public:
147
148 void init();
149
150 // Intentionally unlocked — polling check, see LWIPRawImpl::ready() comment.
151 bool ready() const { return this->accepted_socket_count_ > 0; }
152
153 std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen);
154 std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
155 return this->accept(addr, addrlen);
156 }
157 int listen(int backlog);
158
159 // Listening sockets don't do I/O
160 ssize_t read(void *, size_t) {
161 errno = ENOTSUP;
162 return -1;
163 }
164 ssize_t write(const void *, size_t) {
165 errno = ENOTSUP;
166 return -1;
167 }
168 ssize_t readv(const struct iovec *, int) {
169 errno = ENOTSUP;
170 return -1;
171 }
172 ssize_t writev(const struct iovec *, int) {
173 errno = ENOTSUP;
174 return -1;
175 }
176 ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
177 errno = ENOTSUP;
178 return -1;
179 }
180 ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
181 errno = ENOTSUP;
182 return -1;
183 }
184 int setblocking(bool) { return 0; }
185 int loop() { return 0; }
186
187 static void s_err_fn(void *arg, err_t err);
188
189 private:
190 err_t accept_fn_(struct tcp_pcb *newpcb, err_t err);
191 static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err);
192
193 // Temporary callbacks for queued PCBs (between accept_fn_ and accept())
194 static void s_queued_err_fn(void *arg, err_t err);
195 static err_t s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
196
197 // Accept queue entry — stores a raw tcp_pcb and any data received while queued.
198 // lwip's default tcp_recv_null handler drops data and ACKs it, so we must register
199 // a temporary recv callback to buffer any data that arrives between accept_fn_
200 // (which stores the PCB) and accept() (which creates the LWIPRawImpl).
201 struct QueuedPcb {
202 struct tcp_pcb *pcb{nullptr};
203 struct pbuf *rx_buf{nullptr}; // Data received while queued (before accept() picks it up)
204 bool rx_closed{false}; // Remote sent FIN while queued
205 };
206
207 // Accept queue — stores raw tcp_pcb entries instead of heap-allocated LWIPRawImpl objects.
208 // LWIPRawImpl creation is deferred to the main-loop accept() call. This avoids:
209 // - Heap allocation in the accept callback (unsafe from IRQ context on RP2040)
210 // - Dangling LWIPRawImpl if the connection errors before accept() picks it up
211 // 2 slots is plenty since the main loop drains the queue every iteration.
212 static constexpr size_t MAX_ACCEPTED_SOCKETS = 2;
213 std::array<QueuedPcb, MAX_ACCEPTED_SOCKETS> accepted_pcbs_{};
214 uint8_t accepted_socket_count_ = 0; // Number of entries currently in queue
215};
216
217} // namespace esphome::socket
218
219#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:1045