From c43015a467253f965d1c8287a0cbf2ef89c0a699 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Mar 2026 21:28:08 -1000 Subject: [PATCH] [ota,socket] Use SO_RCVTIMEO for OTA data transfer instead of polling Replace the non-blocking poll + delay(1) pattern in OTA data transfer with SO_RCVTIMEO blocking reads. The socket now wakes immediately when data arrives instead of sleeping 1ms between polls. Adds SO_RCVTIMEO support to the raw TCP socket implementation (ESP8266, RP2040) using the existing socket_delay()/socket_wake() infrastructure. The timeout is stored as a uint8_t in centiseconds, fitting in existing struct padding with zero RAM cost. Tested OTA improvements across platforms: - ESP32-S3: ~15% faster (6.96-7.76s -> 5.87-6.60s) - LibreTiny RTL: 24% faster (18.84s -> 14.33s) - LibreTiny BK72xx: 56% faster (55.52s -> 24.38s) - ESP8266: ~1% faster (compressed OTA, already efficient) --- .../components/esphome/ota/ota_esphome.cpp | 14 ++++++- esphome/components/socket/headers.h | 1 + .../components/socket/lwip_raw_tcp_impl.cpp | 42 ++++++++++++++++++- esphome/components/socket/lwip_raw_tcp_impl.h | 8 ++-- 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index a1cdf59d2b7..b84bfe67917 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -18,6 +18,7 @@ #include #include +#include namespace esphome { @@ -249,6 +250,16 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif + // Switch to blocking mode with receive timeout for efficient data transfer. + // This replaces the non-blocking poll + delay(1) pattern: read() now sleeps + // until data arrives (waking immediately) instead of polling every 1ms. + // The 2-second timeout ensures the WDT is fed regularly (WDT is typically 5s). + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + this->client_->setblocking(true); + // Acknowledge auth OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); @@ -299,7 +310,8 @@ void ESPHomeOTAComponent::handle_data_() { ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (this->would_block_(errno)) { - this->yield_and_feed_watchdog_(); + // read() already waited up to SO_RCVTIMEO for data, just feed WDT + App.feed_wdt(); continue; } ESP_LOGW(TAG, "Read err %d", errno); diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 16e4d23d3ba..c3f7e1e0467 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -51,6 +51,7 @@ #define SO_REUSEADDR 0x0004 /* Allow local address reuse */ #define SO_KEEPALIVE 0x0008 /* keep connections alive */ #define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */ +#define SO_RCVTIMEO 0x1006 /* receive timeout */ #define SOL_SOCKET 0xfff /* options for socket level */ diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 445a57809d2..7995e83d245 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -303,6 +304,18 @@ int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *o *optlen = 4; return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (*optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + uint32_t ms = this->recv_timeout_cs_ * 10; + auto *tv = reinterpret_cast(optval); + tv->tv_sec = ms / 1000; + tv->tv_usec = (ms % 1000) * 1000; + *optlen = sizeof(struct timeval); + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (*optlen < 4) { errno = EINVAL; @@ -331,6 +344,17 @@ int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, sockle // to prevent warnings return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + const auto *tv = reinterpret_cast(optval); + uint32_t ms = tv->tv_sec * 1000 + tv->tv_usec / 1000; + uint32_t cs = (ms + 9) / 10; // round up to nearest centisecond + this->recv_timeout_cs_ = cs > 255 ? 255 : static_cast(cs); + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (optlen != 4) { errno = EINVAL; @@ -459,8 +483,22 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) { return 0; } if (this->rx_buf_ == nullptr) { - errno = EWOULDBLOCK; - return -1; + if (this->recv_timeout_cs_ > 0) { + // Wait efficiently for data — socket_delay() sleeps and wakes + // immediately when recv_fn() fires (data arrives via socket_wake()) + socket_delay(this->recv_timeout_cs_ * 10); + // Recheck after waking — data or close may have arrived + if (this->rx_closed_ && this->rx_buf_ == nullptr) + return 0; + if (this->rx_buf_ == nullptr) { + errno = EWOULDBLOCK; + return -1; + } + // Data arrived, fall through to copy + } else { + errno = EWOULDBLOCK; + return -1; + } } size_t read = 0; diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index c171e0537f3..ca8ac1df17a 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -57,6 +57,7 @@ class LWIPRawCommon { // 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. @@ -102,11 +103,8 @@ class LWIPRawImpl : public LWIPRawCommon { errno = ECONNRESET; return -1; } - if (blocking) { - // blocking operation not supported - errno = EINVAL; - return -1; - } + // Raw TCP doesn't use a blocking flag directly. Blocking behavior + // is provided by SO_RCVTIMEO which makes read() wait via socket_delay(). return 0; } int loop() { return 0; }