mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[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)
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <sys/time.h>
|
||||
|
||||
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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <sys/time.h>
|
||||
|
||||
#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<struct timeval *>(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<const struct timeval *>(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<uint8_t>(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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user