From 8edaf1f53c898d0df804a32ec279567cf1f2a398 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:59:05 +1100 Subject: [PATCH] [tcp] Keep the receive buffer in static memory Code generation declares the TCP link's receive buffer as a static array of max_packet_size bytes, so it is not allocated at run time. Co-Authored-By: Claude Opus 5.5 --- esphome/components/tcp/packet_interface/__init__.py | 8 +++++++- .../tcp/packet_interface/tcp_packet_interface.cpp | 7 +++---- .../tcp/packet_interface/tcp_packet_interface.h | 7 +++---- tests/components/tcp/tcp_packet_interface.cpp | 5 ++++- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/tcp/packet_interface/__init__.py b/esphome/components/tcp/packet_interface/__init__.py index 0d5b2e3630a..7bf86ab3598 100644 --- a/esphome/components/tcp/packet_interface/__init__.py +++ b/esphome/components/tcp/packet_interface/__init__.py @@ -8,6 +8,7 @@ from esphome.components.const import ( CONF_ROLE, ) from esphome.components.packet_interface import ( + CONF_MAX_PACKET_SIZE, new_packet_interface, packet_interface_schema, ) @@ -81,7 +82,12 @@ CONFIG_SCHEMA = cv.typed_schema( async def to_code(config: ConfigType) -> None: - var = await new_packet_interface(config) + # The receive buffer's size is known here, so it is a static array rather than allocated at run time + rx_buffer = f"{config[CONF_ID].id}__rx_buffer" + cg.add_global( + cg.RawStatement(f"static uint8_t {rx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];") + ) + var = await new_packet_interface(config, cg.RawExpression(rx_buffer)) if config[CONF_ROLE] == "server": socket.require_tcp_listener() cg.add(var.set_server(True)) diff --git a/esphome/components/tcp/packet_interface/tcp_packet_interface.cpp b/esphome/components/tcp/packet_interface/tcp_packet_interface.cpp index d109c25fae7..9fc0082145b 100644 --- a/esphome/components/tcp/packet_interface/tcp_packet_interface.cpp +++ b/esphome/components/tcp/packet_interface/tcp_packet_interface.cpp @@ -15,13 +15,12 @@ static constexpr size_t LENGTH_SIZE = 2; // The peer is gone when nothing has been received for this many keep-alive intervals static constexpr uint32_t KEEP_ALIVE_MISSES = 3; -TcpPacketInterface::TcpPacketInterface() { +TcpPacketInterface::TcpPacketInterface(uint8_t *rx_buffer) : rx_buffer_(rx_buffer) { this->capabilities_ = packet_interface::CAPABILITY_RELIABILITY | packet_interface::CAPABILITY_SESSION | packet_interface::CAPABILITY_ADDRESSING; } void TcpPacketInterface::setup() { - this->rx_buffer_ = std::make_unique(this->max_packet_size_); this->link_.begin(TAG); #ifdef USE_SOCKET_TCP_LISTENER this->listener_.begin(TAG); @@ -108,7 +107,7 @@ void TcpPacketInterface::read_() { if (this->rx_have_ < LENGTH_SIZE) { count = this->link_.read(this->rx_header_ + this->rx_have_, LENGTH_SIZE - this->rx_have_); } else { - count = this->link_.read(this->rx_buffer_.get() + this->rx_have_ - LENGTH_SIZE, + count = this->link_.read(this->rx_buffer_ + this->rx_have_ - LENGTH_SIZE, this->rx_length_ + LENGTH_SIZE - this->rx_have_); } // Nothing more for now; a dropped link is seen on the next loop @@ -134,7 +133,7 @@ void TcpPacketInterface::read_() { this->rx_have_ = 0; // A zero length packet is a keep-alive if (this->rx_length_ != 0) - this->on_receive_from_interface_(PacketBuffer(this->rx_buffer_.get(), this->rx_length_), {}); + this->on_receive_from_interface_(PacketBuffer(this->rx_buffer_, this->rx_length_), {}); } } } diff --git a/esphome/components/tcp/packet_interface/tcp_packet_interface.h b/esphome/components/tcp/packet_interface/tcp_packet_interface.h index 6cb4360a55e..228495fc3e6 100644 --- a/esphome/components/tcp/packet_interface/tcp_packet_interface.h +++ b/esphome/components/tcp/packet_interface/tcp_packet_interface.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "esphome/components/packet_interface/packet_interface.h" #include "esphome/components/socket/tcp_client_link.h" @@ -31,7 +30,8 @@ namespace esphome::tcp { */ class TcpPacketInterface : public packet_interface::PacketInterface { public: - TcpPacketInterface(); + /// @param rx_buffer Holds max_packet_size bytes of the packet being received; must outlive the link + explicit TcpPacketInterface(uint8_t *rx_buffer); void setup() override; void loop() override; @@ -69,8 +69,7 @@ class TcpPacketInterface : public packet_interface::PacketInterface { #ifdef USE_BINARY_SENSOR binary_sensor::BinarySensor *connected_sensor_{nullptr}; #endif - // The packet being received, allocated in setup() - std::unique_ptr rx_buffer_; + uint8_t *const rx_buffer_; uint32_t keep_alive_ms_{10000}; uint32_t last_rx_ms_{0}; uint32_t last_tx_ms_{0}; diff --git a/tests/components/tcp/tcp_packet_interface.cpp b/tests/components/tcp/tcp_packet_interface.cpp index 0f51b336d64..4f0182f7cc2 100644 --- a/tests/components/tcp/tcp_packet_interface.cpp +++ b/tests/components/tcp/tcp_packet_interface.cpp @@ -27,12 +27,15 @@ using Packet = std::vector; class TestTcp : public TcpPacketInterface { public: - TestTcp() { + TestTcp() : TcpPacketInterface(this->rx_buffer_storage) { this->set_max_packet_size(64); this->set_host("peer"); this->set_port(1); } socket::TcpClientLink &link() { return this->link_; } + + // The base class only keeps the pointer, so the storage can be a member set up after it + uint8_t rx_buffer_storage[64]{}; }; // A layer stacked on the link, to check that the session state reaches it