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