diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dddf21f57ed..cf9fa8e7c05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -868,7 +868,8 @@ jobs: python script/test_build_components.py \ -e compile \ -c "$component_list" \ - -t "$platform" 2>&1 | \ + -t "$platform" \ + --base-only 2>&1 | \ tee /dev/stderr | \ python script/ci_memory_impact_extract.py \ --output-env \ @@ -954,7 +955,8 @@ jobs: python script/test_build_components.py \ -e compile \ -c "$component_list" \ - -t "$platform" 2>&1 | \ + -t "$platform" \ + --base-only 2>&1 | \ tee /dev/stderr | \ python script/ci_memory_impact_extract.py \ --output-env \ diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index d1215388d2c..f98eca80766 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -195,7 +195,10 @@ class APIFrameHelper { } // Get the frame footer size required by this protocol uint8_t frame_footer_size() const { return frame_footer_size_; } - // Check if socket has data ready to read + // Check if socket has buffered data ready to read. + // Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK) + // or track that they stopped early and retry without this check. + // See Socket::ready() for details. bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } // Release excess memory from internal buffers after initial sync void release_buffers() { diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 149973ba8a2..0ee718cd20e 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -7,7 +7,7 @@ namespace gdk101 { static const char *const TAG = "gdk101"; static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5; -static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 10; +static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 30; static constexpr uint32_t RESET_INTERVAL_ID = 0; static constexpr uint32_t RESET_INTERVAL_MS = 1000; diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index 339a699bc97..e520784702b 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -112,6 +112,8 @@ class BSDSocketImpl { int setblocking(bool blocking); int loop() { return 0; } + /// Check if the socket has buffered data ready to read. + /// See the ready() contract in socket.h — callers must drain or track remaining data. bool ready() const; int get_fd() const { return this->fd_; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index e2dcb80d32b..917b5b2f7a2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -96,6 +96,8 @@ class LWIPRawImpl : public LWIPRawCommon { 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. diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index bfc4da9926a..942d0ccf857 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -78,6 +78,8 @@ class LwIPSocketImpl { int setblocking(bool blocking); int loop() { return 0; } + /// Check if the socket has buffered data ready to read. + /// See the ready() contract in socket.h — callers must drain or track remaining data. bool ready() const; int get_fd() const { return this->fd_; } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 9ea71321e0b..ad55e889e80 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -53,6 +53,19 @@ bool socket_ready_fd(int fd, bool loop_monitored); // Inline ready() — defined here because it depends on socket_ready/socket_ready_fd // declared above, while the impl headers are included before those declarations. +// +// Contract (applies to ALL socket implementations — each platform implements +// ready() differently, but this contract holds regardless of the mechanism): +// ready() checks if the socket has buffered data ready to read. When it returns +// true, the caller MUST read until it would block (EAGAIN/EWOULDBLOCK), or until +// read() returns 0 to indicate EOF / connection closed, or track that it stopped +// early and retry without calling ready(). The next call to ready() will only +// report new data correctly if all callers fulfill this contract. Failing to +// drain the socket may cause ready() to return false while data remains readable. +// +// In practice each socket is owned by a single component, so this contract is +// straightforward to fulfill — but the owning component must be aware of it, +// especially if it limits how many messages it processes per loop iteration. #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) inline bool Socket::ready() const { #ifdef USE_LWIP_FAST_SELECT diff --git a/requirements.txt b/requirements.txt index c4b90b5ca94..d7db44454cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.12.0 +aioesphomeapi==44.13.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index 1f9d05fc33d..d9a9d158a3e 100644 --- a/tests/benchmarks/core/bench_helpers.cpp +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -72,8 +72,9 @@ BENCHMARK(FormatHexTo_16Bytes); static void FormatHexTo_100Bytes(benchmark::State &state) { uint8_t data[100]; - for (int i = 0; i < 100; i++) + for (int i = 0; i < 100; i++) { data[i] = static_cast(i); + } char buffer[201]; // 100 * 2 + 1 for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -183,12 +184,14 @@ BENCHMARK(Fnv1aHash_Long); // --- fnv1_hash_object_id() - typical entity name --- static void Fnv1HashObjectId(benchmark::State &state) { - const char *name = "Living Room Temperature Sensor"; - size_t len = strlen(name); + char name[] = "Living Room Temperature Sensor"; + size_t len = sizeof(name) - 1; + benchmark::DoNotOptimize(name); for (auto _ : state) { uint32_t result = 0; for (int i = 0; i < kInnerIterations; i++) { result ^= fnv1_hash_object_id(name, len); + benchmark::ClobberMemory(); } benchmark::DoNotOptimize(result); } @@ -259,7 +262,7 @@ BENCHMARK(CRC16_8Bytes); // --- value_accuracy_to_buf() - typical sensor value --- static void ValueAccuracyToBuf(benchmark::State &state) { - char raw_buf[VALUE_ACCURACY_MAX_LEN]; + char raw_buf[VALUE_ACCURACY_MAX_LEN] = {}; std::span buf(raw_buf); float value = 23.456f; for (auto _ : state) { @@ -275,12 +278,13 @@ BENCHMARK(ValueAccuracyToBuf); // --- int8_to_str() --- static void Int8ToStr(benchmark::State &state) { - char buffer[5]; + char buffer[5] = {}; for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { int8_to_str(buffer, static_cast(i & 0xFF)); + benchmark::DoNotOptimize(buffer); + benchmark::ClobberMemory(); } - benchmark::DoNotOptimize(buffer); } state.SetItemsProcessed(state.iterations() * kInnerIterations); }