ESPHome 2026.10.2
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);
54 bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const;
55
56 // Member ordering optimized to minimize padding on 32-bit systems
57 struct tcp_pcb *pcb_;
58 // don't use lwip nodelay flag, it sometimes causes reconnect
59 // instead use it for determining whether to call lwip_output
60 bool nodelay_ = false;
62 uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
63 // 0 before connect(), EINPROGRESS while pending, EISCONN once established,
64 // else the failure errno the callbacks recorded; fills the padding byte
65 uint8_t connect_err_ = 0;
66 static_assert(EINPROGRESS < 256 && EISCONN < 256 && ECONNREFUSED < 256 && ECONNRESET < 256 && ETIMEDOUT < 256,
67 "connect_err_ stores errno values in a byte");
68};
69// The connect state must stay in the padding so no socket pays RAM for it
70static_assert(sizeof(LWIPRawCommon) == sizeof(struct tcp_pcb *) + 4, "LWIPRawCommon grew past one word of flags");
71
74class LWIPRawImpl : public LWIPRawCommon {
75 public:
78
79 void init(struct pbuf *initial_rx = nullptr, bool initial_rx_closed = false);
80
81 // Non-listening sockets return error
82 std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) {
83 errno = EINVAL;
84 return nullptr;
85 }
86 std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
87 return this->accept(addr, addrlen);
88 }
89 // Regular sockets can't be converted to listening - this shouldn't happen
90 // as listen() should only be called on sockets created for listening
91 int listen(int) {
92 errno = EOPNOTSUPP;
93 return -1;
94 }
97 int connect(const struct sockaddr *addr, socklen_t addrlen);
98 // Unlocked like ready(): the callbacks write the error byte before pcb_,
99 // so a torn read only costs one extra poll
100 ConnectPollResult poll_connect(int &err_out) const;
101 ssize_t read(void *buf, size_t len);
102 ssize_t readv(const struct iovec *iov, int iovcnt);
103 ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
104 errno = ENOTSUP;
105 return -1;
106 }
107 ssize_t write(const void *buf, size_t len);
108 ssize_t writev(const struct iovec *iov, int iovcnt);
109 ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
110 // return ::sendto(fd_, buf, len, flags, to, tolen);
111 errno = ENOSYS;
112 return -1;
113 }
114 // Check if the socket has buffered data ready to read.
115 // See the ready() contract in socket.h — callers must drain or track remaining data.
116 // Intentionally unlocked — this is a polling check called every loop iteration.
117 // A stale read at worst delays processing by one loop tick; the actual I/O in
118 // read() holds the lwip lock and re-checks properly. See esphome#10681.
119 bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
120
121 // No lock needed — only called during setup before callbacks are registered.
122 // A stale pcb_ read is benign (returns ECONNRESET, which the caller handles).
123 int setblocking(bool blocking) {
124 if (this->pcb_ == nullptr) {
125 errno = ECONNRESET;
126 return -1;
127 }
128 // Raw TCP doesn't use a blocking flag directly. Blocking behavior
129 // is provided by SO_RCVTIMEO which makes read() wait via wakeable_delay().
130 return 0;
131 }
132 int loop() { return 0; }
133
134 err_t recv_fn(struct pbuf *pb, err_t err);
135
136 static void s_err_fn(void *arg, err_t err);
137 static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
138 static err_t s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err);
139
140 protected:
141 // True when the socket could receive data but none has arrived yet.
142 // Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool,
143 // all atomic on ARM/Xtensa. A stale value is harmless: the caller either does
144 // an unnecessary wait (stale true) or skips it (stale false), and the
145 // authoritative recheck happens under LWIP_LOCK afterward.
146 bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; }
147 void wait_for_data_();
148 ssize_t read_locked_(void *buf, size_t len);
149 ssize_t internal_write_(const void *buf, size_t len);
150 int internal_output_();
151
152 pbuf *rx_buf_ = nullptr;
153 size_t rx_buf_offset_ = 0;
154 bool rx_closed_ = false;
155};
156// rx_buf_, rx_buf_offset_, then rx_closed_ padded to a word
157static_assert(sizeof(LWIPRawImpl) == sizeof(LWIPRawCommon) + sizeof(pbuf *) + sizeof(size_t) + 4,
158 "LWIPRawImpl layout changed");
159
163 public:
166
167 void init();
168
169 // Intentionally unlocked — polling check, see LWIPRawImpl::ready() comment.
170 bool ready() const { return this->accepted_socket_count_ > 0; }
171
172 std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen);
173 std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
174 return this->accept(addr, addrlen);
175 }
176 int listen(int backlog);
177
178 // Listening sockets don't do I/O
179 ssize_t read(void *, size_t) {
180 errno = ENOTSUP;
181 return -1;
182 }
183 ssize_t write(const void *, size_t) {
184 errno = ENOTSUP;
185 return -1;
186 }
187 ssize_t readv(const struct iovec *, int) {
188 errno = ENOTSUP;
189 return -1;
190 }
191 ssize_t writev(const struct iovec *, int) {
192 errno = ENOTSUP;
193 return -1;
194 }
195 ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
196 errno = ENOTSUP;
197 return -1;
198 }
199 ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
200 errno = ENOTSUP;
201 return -1;
202 }
203 int setblocking(bool) { return 0; }
204 int loop() { return 0; }
205
206 static void s_err_fn(void *arg, err_t err);
207
208 private:
209 err_t accept_fn_(struct tcp_pcb *newpcb, err_t err);
210 static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err);
211
212 // Temporary callbacks for queued PCBs (between accept_fn_ and accept())
213 static void s_queued_err_fn(void *arg, err_t err);
214 static err_t s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
215
216 // Accept queue entry — stores a raw tcp_pcb and any data received while queued.
217 // lwip's default tcp_recv_null handler drops data and ACKs it, so we must register
218 // a temporary recv callback to buffer any data that arrives between accept_fn_
219 // (which stores the PCB) and accept() (which creates the LWIPRawImpl).
220 struct QueuedPcb {
221 struct tcp_pcb *pcb{nullptr};
222 struct pbuf *rx_buf{nullptr}; // Data received while queued (before accept() picks it up)
223 bool rx_closed{false}; // Remote sent FIN while queued
224 };
225
226 // Accept queue — stores raw tcp_pcb entries instead of heap-allocated LWIPRawImpl objects.
227 // LWIPRawImpl creation is deferred to the main-loop accept() call. This avoids:
228 // - Heap allocation in the accept callback (unsafe from IRQ context on RP2040)
229 // - Dangling LWIPRawImpl if the connection errors before accept() picks it up
230 // 2 slots is plenty since the main loop drains the queue every iteration.
231 static constexpr size_t MAX_ACCEPTED_SOCKETS = 2;
232 std::array<QueuedPcb, MAX_ACCEPTED_SOCKETS> accepted_pcbs_{};
233 uint8_t accepted_socket_count_ = 0; // Number of entries currently in queue
234};
235
236} // namespace esphome::socket
237
238#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
bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const
sockaddr of this socket's family to lwip address and port; false with errno on mismatch
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)
ConnectPollResult poll_connect(int &err_out) const
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *)
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)
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)
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.
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
ConnectPollResult
Outcome of polling a non-blocking connect(); see socket::poll_connect().
Definition headers.h:209
const void size_t len
Definition hal.h:64