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,
|
size_t,
|
||||||
std_ns,
|
std_ns,
|
||||||
std_shared_ptr,
|
std_shared_ptr,
|
||||||
|
std_span,
|
||||||
std_string,
|
std_string,
|
||||||
std_string_ref,
|
std_string_ref,
|
||||||
std_vector,
|
std_vector,
|
||||||
|
|||||||
@@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) {
|
|||||||
/**
|
/**
|
||||||
* Process a received packet
|
* 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_;
|
auto ping_key_seen = !this->ping_pong_enable_;
|
||||||
PacketDecoder decoder((data.data()), data.size());
|
PacketDecoder decoder(data.data(), data.size());
|
||||||
char namebuf[256]{};
|
char namebuf[256]{};
|
||||||
uint8_t byte;
|
uint8_t byte;
|
||||||
FuData rdata{};
|
FuData rdata{};
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Providing packet encoding functions for exchanging data with a remote host.
|
* 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; }
|
virtual bool should_send() { return true; }
|
||||||
|
|
||||||
// to be called by child classes when a data packet is received.
|
// 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 send_data_(bool all);
|
||||||
void flush_();
|
void flush_();
|
||||||
void add_data_(uint8_t key, const char *id, float data);
|
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")
|
udp_ns = cg.esphome_ns.namespace("udp")
|
||||||
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
||||||
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
||||||
trigger_args = cg.std_vector.template(cg.uint8)
|
|
||||||
trigger_argname = "data"
|
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)]
|
trigger_argtype = [(trigger_args, trigger_argname)]
|
||||||
|
|
||||||
CONF_ADDRESSES = "addresses"
|
CONF_ADDRESSES = "addresses"
|
||||||
@@ -118,7 +122,13 @@ async def to_code(config):
|
|||||||
trigger_id, trigger_argtype, on_receive
|
trigger_id, trigger_argtype, on_receive
|
||||||
)
|
)
|
||||||
trigger_lambda = await cg.process_lambda(
|
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.add_listener(trigger_lambda))
|
||||||
cg.add(var.set_should_listen())
|
cg.add(var.set_should_listen())
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); }
|
|||||||
void UDPTransport::setup() {
|
void UDPTransport::setup() {
|
||||||
PacketTransport::setup();
|
PacketTransport::setup();
|
||||||
if (!this->providers_.empty() || this->is_encrypted_()) {
|
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() {
|
void UDPComponent::loop() {
|
||||||
auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE);
|
|
||||||
if (this->should_listen_) {
|
if (this->should_listen_) {
|
||||||
|
std::array<uint8_t, MAX_PACKET_SIZE> buf;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||||
@@ -116,9 +116,9 @@ void UDPComponent::loop() {
|
|||||||
#endif
|
#endif
|
||||||
if (len <= 0)
|
if (len <= 0)
|
||||||
break;
|
break;
|
||||||
buf.resize(len);
|
size_t packet_len = static_cast<size_t>(len);
|
||||||
ESP_LOGV(TAG, "Received packet of length %zu", len);
|
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||||
this->packet_listeners_.call(buf);
|
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||||
#include <WiFiUdp.h>
|
#include <WiFiUdp.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <array>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome::udp {
|
namespace esphome::udp {
|
||||||
@@ -26,7 +28,7 @@ class UDPComponent : public Component {
|
|||||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||||
void set_should_broadcast() { this->should_broadcast_ = true; }
|
void set_should_broadcast() { this->should_broadcast_ = true; }
|
||||||
void set_should_listen() { this->should_listen_ = 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));
|
this->packet_listeners_.add(std::move(listener));
|
||||||
}
|
}
|
||||||
void setup() override;
|
void setup() override;
|
||||||
@@ -41,7 +43,7 @@ class UDPComponent : public Component {
|
|||||||
uint16_t broadcast_port_{};
|
uint16_t broadcast_port_{};
|
||||||
bool should_broadcast_{};
|
bool should_broadcast_{};
|
||||||
bool should_listen_{};
|
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)
|
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||||
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;
|
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 = std_ns.class_("string")
|
||||||
std_string_ref = std_ns.namespace("string &")
|
std_string_ref = std_ns.namespace("string &")
|
||||||
std_vector = std_ns.class_("vector")
|
std_vector = std_ns.class_("vector")
|
||||||
|
std_span = std_ns.class_("span")
|
||||||
uint8 = global_ns.namespace("uint8_t")
|
uint8 = global_ns.namespace("uint8_t")
|
||||||
uint16 = global_ns.namespace("uint16_t")
|
uint16 = global_ns.namespace("uint16_t")
|
||||||
uint32 = global_ns.namespace("uint32_t")
|
uint32 = global_ns.namespace("uint32_t")
|
||||||
|
|||||||
@@ -93,23 +93,34 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]
|
|||||||
sock.close()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_udp_send_receive(
|
async def test_udp_send_receive(
|
||||||
yaml_config: str,
|
yaml_config: str,
|
||||||
run_compiled: RunCompiledFunction,
|
run_compiled: RunCompiledFunction,
|
||||||
api_client_connected: APIClientConnectedFactory,
|
api_client_connected: APIClientConnectedFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test UDP component can send messages with multiple addresses configured."""
|
"""Test UDP component can send and receive messages."""
|
||||||
# Track log lines to verify dump_config output
|
|
||||||
log_lines: list[str] = []
|
log_lines: list[str] = []
|
||||||
|
receive_event = asyncio.Event()
|
||||||
|
|
||||||
def on_log_line(line: str) -> None:
|
def on_log_line(line: str) -> None:
|
||||||
log_lines.append(line)
|
log_lines.append(line)
|
||||||
|
if "Received UDP:" in line:
|
||||||
|
receive_event.set()
|
||||||
|
|
||||||
async with udp_listener() as (udp_port, receiver):
|
async with udp_listener() as (broadcast_port, receiver):
|
||||||
# Replace placeholders in the config
|
listen_port = _get_free_udp_port()
|
||||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
|
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port))
|
||||||
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
|
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port))
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
run_compiled(config, line_callback=on_log_line),
|
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, (
|
assert "Address: 127.0.0.2" in log_text, (
|
||||||
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
|
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