mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 00:31:58 +00:00
[udp] Eliminate per-loop heap allocation by using std::span for packet callbacks
Replace std::vector<uint8_t> with std::array on the stack for the receive buffer and std::span<const uint8_t> for packet listener callbacks. This eliminates a 508-byte heap allocation on every loop() iteration, reducing heap fragmentation on long-running devices. The callback signature changes from std::vector<uint8_t>& to std::span<const uint8_t>, which preserves API compatibility for user lambdas using data.size(), data.data(), data[i], and range-for. The trigger/automation type remains std::vector<uint8_t> so that deferred actions (e.g. delay) safely own their data. The listener lambda converts span to vector at the trigger boundary. PacketTransport::process_() now takes std::span<const uint8_t> directly, with implicit conversion from std::vector for other transports.
This commit is contained in:
@@ -87,6 +87,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
size_t,
|
||||
std_ns,
|
||||
std_shared_ptr,
|
||||
std_span,
|
||||
std_string,
|
||||
std_string_ref,
|
||||
std_vector,
|
||||
|
||||
@@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) {
|
||||
/**
|
||||
* Process a received packet
|
||||
*/
|
||||
void PacketTransport::process_(const std::vector<uint8_t> &data) {
|
||||
void PacketTransport::process_(std::span<const uint8_t> data) {
|
||||
auto ping_key_seen = !this->ping_pong_enable_;
|
||||
PacketDecoder decoder((data.data()), data.size());
|
||||
PacketDecoder decoder(data.data(), data.size());
|
||||
char namebuf[256]{};
|
||||
uint8_t byte;
|
||||
FuData rdata{};
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Providing packet encoding functions for exchanging data with a remote host.
|
||||
@@ -113,7 +114,7 @@ class PacketTransport : public PollingComponent {
|
||||
virtual bool should_send() { return true; }
|
||||
|
||||
// to be called by child classes when a data packet is received.
|
||||
void process_(const std::vector<uint8_t> &data);
|
||||
void process_(std::span<const uint8_t> data);
|
||||
void send_data_(bool all);
|
||||
void flush_();
|
||||
void add_data_(uint8_t key, const char *id, float data);
|
||||
|
||||
@@ -23,8 +23,12 @@ MULTI_CONF = True
|
||||
udp_ns = cg.esphome_ns.namespace("udp")
|
||||
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
||||
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
||||
trigger_args = cg.std_vector.template(cg.uint8)
|
||||
trigger_argname = "data"
|
||||
# Listener callback type (non-owning span from UDP component)
|
||||
listener_args = cg.std_span.template(cg.uint8.operator("const"))
|
||||
listener_argtype = [(listener_args, trigger_argname)]
|
||||
# Automation/trigger type (owned vector, safe for deferred actions like delay)
|
||||
trigger_args = cg.std_vector.template(cg.uint8)
|
||||
trigger_argtype = [(trigger_args, trigger_argname)]
|
||||
|
||||
CONF_ADDRESSES = "addresses"
|
||||
@@ -118,7 +122,13 @@ async def to_code(config):
|
||||
trigger_id, trigger_argtype, on_receive
|
||||
)
|
||||
trigger_lambda = await cg.process_lambda(
|
||||
trigger.trigger(literal(trigger_argname)), trigger_argtype
|
||||
trigger.trigger(
|
||||
cg.std_vector.template(cg.uint8)(
|
||||
literal(f"{trigger_argname}.begin()"),
|
||||
literal(f"{trigger_argname}.end()"),
|
||||
)
|
||||
),
|
||||
listener_argtype,
|
||||
)
|
||||
cg.add(var.add_listener(trigger_lambda))
|
||||
cg.add(var.set_should_listen())
|
||||
|
||||
@@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); }
|
||||
void UDPTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
if (!this->providers_.empty() || this->is_encrypted_()) {
|
||||
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); });
|
||||
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ void UDPComponent::setup() {
|
||||
}
|
||||
|
||||
void UDPComponent::loop() {
|
||||
auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE);
|
||||
if (this->should_listen_) {
|
||||
std::array<uint8_t, MAX_PACKET_SIZE> buf;
|
||||
for (;;) {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||
@@ -116,9 +116,9 @@ void UDPComponent::loop() {
|
||||
#endif
|
||||
if (len <= 0)
|
||||
break;
|
||||
buf.resize(len);
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", len);
|
||||
this->packet_listeners_.call(buf);
|
||||
size_t packet_len = static_cast<size_t>(len);
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::udp {
|
||||
@@ -26,7 +28,7 @@ class UDPComponent : public Component {
|
||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||
void set_should_broadcast() { this->should_broadcast_ = true; }
|
||||
void set_should_listen() { this->should_listen_ = true; }
|
||||
void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) {
|
||||
void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) {
|
||||
this->packet_listeners_.add(std::move(listener));
|
||||
}
|
||||
void setup() override;
|
||||
@@ -41,7 +43,7 @@ class UDPComponent : public Component {
|
||||
uint16_t broadcast_port_{};
|
||||
bool should_broadcast_{};
|
||||
bool should_listen_{};
|
||||
CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{};
|
||||
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;
|
||||
|
||||
@@ -12,6 +12,7 @@ std_shared_ptr = std_ns.class_("shared_ptr")
|
||||
std_string = std_ns.class_("string")
|
||||
std_string_ref = std_ns.namespace("string &")
|
||||
std_vector = std_ns.class_("vector")
|
||||
std_span = std_ns.class_("span")
|
||||
uint8 = global_ns.namespace("uint8_t")
|
||||
uint16 = global_ns.namespace("uint16_t")
|
||||
uint32 = global_ns.namespace("uint32_t")
|
||||
|
||||
@@ -93,23 +93,34 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]
|
||||
sock.close()
|
||||
|
||||
|
||||
def _get_free_udp_port() -> int:
|
||||
"""Get a free UDP port by binding to port 0 and releasing."""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
return port
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_udp_send_receive(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test UDP component can send messages with multiple addresses configured."""
|
||||
# Track log lines to verify dump_config output
|
||||
"""Test UDP component can send and receive messages."""
|
||||
log_lines: list[str] = []
|
||||
receive_event = asyncio.Event()
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
if "Received UDP:" in line:
|
||||
receive_event.set()
|
||||
|
||||
async with udp_listener() as (udp_port, receiver):
|
||||
# Replace placeholders in the config
|
||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
|
||||
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
|
||||
async with udp_listener() as (broadcast_port, receiver):
|
||||
listen_port = _get_free_udp_port()
|
||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port))
|
||||
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port))
|
||||
|
||||
async with (
|
||||
run_compiled(config, line_callback=on_log_line),
|
||||
@@ -169,3 +180,19 @@ async def test_udp_send_receive(
|
||||
assert "Address: 127.0.0.2" in log_text, (
|
||||
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
|
||||
)
|
||||
|
||||
# Test receiving a UDP packet (exercises on_receive with std::span)
|
||||
test_payload = b"TEST_RECEIVE_UDP"
|
||||
send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
send_sock.sendto(test_payload, ("127.0.0.1", listen_port))
|
||||
finally:
|
||||
send_sock.close()
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(receive_event.wait(), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_receive did not fire. Expected 'Received UDP:' in logs. "
|
||||
f"Last log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user