220 lines
7.9 KiB
C++
220 lines
7.9 KiB
C++
#pragma once
|
|
#include "esphome/core/defines.h"
|
|
|
|
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
|
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <span>
|
|
|
|
#include "esphome/core/helpers.h"
|
|
#include "headers.h"
|
|
#include "lwip/ip.h"
|
|
#include "lwip/netif.h"
|
|
#include "lwip/opt.h"
|
|
#include "lwip/tcp.h"
|
|
|
|
namespace esphome::socket {
|
|
|
|
// Forward declaration
|
|
class LWIPRawImpl;
|
|
|
|
/// Non-virtual common base for LWIP raw TCP sockets.
|
|
/// Provides shared fields and methods for both connected and listening sockets.
|
|
/// No virtual methods — pure code sharing.
|
|
class LWIPRawCommon {
|
|
public:
|
|
LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {}
|
|
~LWIPRawCommon();
|
|
LWIPRawCommon(const LWIPRawCommon &) = delete;
|
|
LWIPRawCommon &operator=(const LWIPRawCommon &) = delete;
|
|
|
|
int bind(const struct sockaddr *name, socklen_t addrlen);
|
|
int close();
|
|
int shutdown(int how);
|
|
|
|
int getpeername(struct sockaddr *name, socklen_t *addrlen);
|
|
int getsockname(struct sockaddr *name, socklen_t *addrlen);
|
|
|
|
/// Format peer address into a fixed-size buffer (no heap allocation)
|
|
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
|
|
/// Format local address into a fixed-size buffer (no heap allocation)
|
|
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
|
|
|
|
int getsockopt(int level, int optname, void *optval, socklen_t *optlen);
|
|
int setsockopt(int level, int optname, const void *optval, socklen_t optlen);
|
|
|
|
int get_fd() const { return -1; }
|
|
|
|
protected:
|
|
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);
|
|
|
|
// Member ordering optimized to minimize padding on 32-bit systems
|
|
struct tcp_pcb *pcb_;
|
|
// don't use lwip nodelay flag, it sometimes causes reconnect
|
|
// instead use it for determining whether to call lwip_output
|
|
bool nodelay_ = false;
|
|
sa_family_t family_ = 0;
|
|
uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
|
|
};
|
|
|
|
/// Connected socket implementation for LWIP raw TCP.
|
|
/// No virtual methods — callers always use the concrete type.
|
|
class LWIPRawImpl : public LWIPRawCommon {
|
|
public:
|
|
using LWIPRawCommon::LWIPRawCommon;
|
|
~LWIPRawImpl();
|
|
|
|
void init(struct pbuf *initial_rx = nullptr, bool initial_rx_closed = false);
|
|
|
|
// Non-listening sockets return error
|
|
std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) {
|
|
errno = EINVAL;
|
|
return nullptr;
|
|
}
|
|
std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
|
|
return this->accept(addr, addrlen);
|
|
}
|
|
// Regular sockets can't be converted to listening - this shouldn't happen
|
|
// as listen() should only be called on sockets created for listening
|
|
int listen(int) {
|
|
errno = EOPNOTSUPP;
|
|
return -1;
|
|
}
|
|
ssize_t read(void *buf, size_t len);
|
|
ssize_t readv(const struct iovec *iov, int iovcnt);
|
|
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
|
|
errno = ENOTSUP;
|
|
return -1;
|
|
}
|
|
ssize_t write(const void *buf, size_t len);
|
|
ssize_t writev(const struct iovec *iov, int iovcnt);
|
|
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
|
|
// return ::sendto(fd_, buf, len, flags, to, tolen);
|
|
errno = ENOSYS;
|
|
return -1;
|
|
}
|
|
// Check if the socket has buffered data ready to read.
|
|
// See the ready() contract in socket.h — callers must drain or track remaining data.
|
|
// Intentionally unlocked — this is a polling check called every loop iteration.
|
|
// A stale read at worst delays processing by one loop tick; the actual I/O in
|
|
// read() holds the lwip lock and re-checks properly. See esphome#10681.
|
|
bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
|
|
|
|
// No lock needed — only called during setup before callbacks are registered.
|
|
// A stale pcb_ read is benign (returns ECONNRESET, which the caller handles).
|
|
int setblocking(bool blocking) {
|
|
if (this->pcb_ == nullptr) {
|
|
errno = ECONNRESET;
|
|
return -1;
|
|
}
|
|
// Raw TCP doesn't use a blocking flag directly. Blocking behavior
|
|
// is provided by SO_RCVTIMEO which makes read() wait via wakeable_delay().
|
|
return 0;
|
|
}
|
|
int loop() { return 0; }
|
|
|
|
err_t recv_fn(struct pbuf *pb, err_t err);
|
|
|
|
static void s_err_fn(void *arg, err_t err);
|
|
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
|
|
|
|
protected:
|
|
// True when the socket could receive data but none has arrived yet.
|
|
// Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool,
|
|
// all atomic on ARM/Xtensa. A stale value is harmless: the caller either does
|
|
// an unnecessary wait (stale true) or skips it (stale false), and the
|
|
// authoritative recheck happens under LWIP_LOCK afterward.
|
|
bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; }
|
|
void wait_for_data_();
|
|
ssize_t read_locked_(void *buf, size_t len);
|
|
ssize_t internal_write_(const void *buf, size_t len);
|
|
int internal_output_();
|
|
|
|
pbuf *rx_buf_ = nullptr;
|
|
size_t rx_buf_offset_ = 0;
|
|
bool rx_closed_ = false;
|
|
};
|
|
|
|
/// Listening socket implementation for LWIP raw TCP.
|
|
/// Separate from LWIPRawImpl — no virtual dispatch needed.
|
|
class LWIPRawListenImpl : public LWIPRawCommon {
|
|
public:
|
|
using LWIPRawCommon::LWIPRawCommon;
|
|
~LWIPRawListenImpl();
|
|
|
|
void init();
|
|
|
|
// Intentionally unlocked — polling check, see LWIPRawImpl::ready() comment.
|
|
bool ready() const { return this->accepted_socket_count_ > 0; }
|
|
|
|
std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen);
|
|
std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
|
|
return this->accept(addr, addrlen);
|
|
}
|
|
int listen(int backlog);
|
|
|
|
// Listening sockets don't do I/O
|
|
ssize_t read(void *, size_t) {
|
|
errno = ENOTSUP;
|
|
return -1;
|
|
}
|
|
ssize_t write(const void *, size_t) {
|
|
errno = ENOTSUP;
|
|
return -1;
|
|
}
|
|
ssize_t readv(const struct iovec *, int) {
|
|
errno = ENOTSUP;
|
|
return -1;
|
|
}
|
|
ssize_t writev(const struct iovec *, int) {
|
|
errno = ENOTSUP;
|
|
return -1;
|
|
}
|
|
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
|
|
errno = ENOTSUP;
|
|
return -1;
|
|
}
|
|
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
|
|
errno = ENOTSUP;
|
|
return -1;
|
|
}
|
|
int setblocking(bool) { return 0; }
|
|
int loop() { return 0; }
|
|
|
|
static void s_err_fn(void *arg, err_t err);
|
|
|
|
private:
|
|
err_t accept_fn_(struct tcp_pcb *newpcb, err_t err);
|
|
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err);
|
|
|
|
// Temporary callbacks for queued PCBs (between accept_fn_ and accept())
|
|
static void s_queued_err_fn(void *arg, err_t err);
|
|
static err_t s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
|
|
|
|
// Accept queue entry — stores a raw tcp_pcb and any data received while queued.
|
|
// lwip's default tcp_recv_null handler drops data and ACKs it, so we must register
|
|
// a temporary recv callback to buffer any data that arrives between accept_fn_
|
|
// (which stores the PCB) and accept() (which creates the LWIPRawImpl).
|
|
struct QueuedPcb {
|
|
struct tcp_pcb *pcb{nullptr};
|
|
struct pbuf *rx_buf{nullptr}; // Data received while queued (before accept() picks it up)
|
|
bool rx_closed{false}; // Remote sent FIN while queued
|
|
};
|
|
|
|
// Accept queue — stores raw tcp_pcb entries instead of heap-allocated LWIPRawImpl objects.
|
|
// LWIPRawImpl creation is deferred to the main-loop accept() call. This avoids:
|
|
// - Heap allocation in the accept callback (unsafe from IRQ context on RP2040)
|
|
// - Dangling LWIPRawImpl if the connection errors before accept() picks it up
|
|
// 2 slots is plenty since the main loop drains the queue every iteration.
|
|
static constexpr size_t MAX_ACCEPTED_SOCKETS = 2;
|
|
std::array<QueuedPcb, MAX_ACCEPTED_SOCKETS> accepted_pcbs_{};
|
|
uint8_t accepted_socket_count_ = 0; // Number of entries currently in queue
|
|
};
|
|
|
|
} // namespace esphome::socket
|
|
|
|
#endif // USE_SOCKET_IMPL_LWIP_TCP
|