Compare commits

...
Author SHA1 Message Date
clydebarrowandClaude Code 32c097dfad [udp] Take the packet size from the copy buffer
The link derives max_packet_size from the span it is given, so a packet
larger than the copy buffer cannot be copied into it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 e633f54997 [udp] Limit a split packet to the copy buffer
The copy buffer is sized by code generation to max_packet_size, but
set_max_packet_size() can raise the limit afterwards, and copying a split
packet would then overflow it. The link now takes the buffer as a span
and refuses a split packet larger than it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 ef6bb95608 [udp] Drop datagrams longer than the receive buffer
A read cuts a datagram longer than the buffer to fit, so a 600 byte
datagram arrived as a valid looking 508 byte packet. The buffer is now one
byte longer, and a read that fills it is dropped. The split packet test
also checks the bytes joined in the copy buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 f128049f99 [packet_interface] Fix clang-tidy findings in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 9f44c65d99 [udp] Keep the copy buffer in static memory
Code generation declares the UDP link's buffer for packets split across
buffers as a static array of max_packet_size bytes, so it is not
allocated at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 3d67696cd5 [packet_interface] Use a local to_vector() in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 9064bd05c5 [udp] Fixes from code review
- Enforce max_packet_size on send and receive
- Copy split packets into a buffer allocated in setup, not on the stack
- Add unit tests for the link's send and receive paths
- Explain why the broadcast socket is always enabled

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 65da10e4d8 [udp] Add UDP packet interface link
Each packet is one UDP datagram, using the settings of the udp component.
The udp component now reports whether a packet was sent, and an ENOMEM
refusal from lwIP is logged at verbose level as the packet can be sent
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00
clydebarrowandClaude Opus 5.5 982eda4bab [packet_interface] Use the shared max_packet_size constant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:26 +11:00
clydebarrowandClaude Code fd78282aa2 [packet_interface] Set the packet size when a link is constructed
max_packet_size was a default with a setter that code generation called
after construction, so it could be raised above the size of a link's
buffer. It is now a constructor parameter, and a link with a fixed buffer
takes the buffer as a std::span, so its size is the link's limit and the
two cannot differ.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
clydebarrowandClaude Opus 5.5 ed6c54efcb [uart] Limit a received packet to the receive buffer
The receive buffer is sized by code generation to max_packet_size, but
set_max_packet_size() can raise the limit afterwards, and the received
bytes would then overflow the buffer. The link now takes the buffer as a
span and stops at its size too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
clydebarrowandClaude Opus 5.5 333090f803 [packet_interface] Keep App alive across the bridge tests
The test main constructs App once, and later tests and its listener use
it, so the fixture now replaces App in SetUp and constructs it again in
TearDown, instead of constructing over a live App and leaving it
destroyed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
clydebarrowandClaude Opus 5.5 627a418d88 [core] Join list entries from several unit test benchmark.yaml files
Each test directory's benchmark.yaml was merged by top-level key, keeping
the first value, so when several directories added entries to one
platform component only the first directory's survived. In a combined
run the UDP link was then left out of the build and its test failed to
compile. Lists are now joined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
clydebarrowandClaude Opus 5.5 14ba5b9c5f [packet_interface] Test the packet_transport bridge
A fake link with a layer stacked on it checks that the bridge sends
through the top of the stack, passes a packet in one buffer to
packet_transport, and drops a packet split across buffers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
clydebarrowandClaude Opus 5.5 726c30dccd [packet_interface] Remove PacketBuffer::to_vector()
It allocated a vector for each call, which invites heap churn. Only the
tests used it; they now have a local helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
clydebarrowandClaude Opus 5.5 574e9f4687 [packet_interface] Keep the UART receive buffer in static memory, fix clang-tidy
- Code generation declares the UART link's receive buffer as a static
  array of max_packet_size bytes, so it is not allocated at run time
- Move the increment out of the condition in note_received_()
- Initialise the UART transmit copy buffer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
clydebarrow bfb4b3ddfe Remove redundant include and property 2026-10-19 14:40:08 +11:00
clydebarrow cedafa5208 Fixes from code review 2026-10-19 14:40:08 +11:00
clydebarrowandClaude Opus 5.5 95f972d3c5 [packet_interface] Add packet interface component
A packet interface moves packets over a channel, with a uart link and a
packet_transport platform that uses any packet interface. Later layers
stack on the same base class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:40:08 +11:00
37 changed files with 1765 additions and 30 deletions
+4
View File
@@ -457,6 +457,8 @@ esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_interface/* @clydebarrow
esphome/components/packet_interface/packet_transport/* @clydebarrow
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
@@ -673,10 +675,12 @@ esphome/components/tuya/water_heater/* @iago-veiga
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_interface/* @clydebarrow
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/uart_mux/* @kbx81
esphome/components/uart_tcp/* @Bascht74
esphome/components/udp/* @clydebarrow
esphome/components/udp/packet_interface/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
esphome/components/ufm01/* @ljungqvist
@@ -0,0 +1,56 @@
from typing import Any
import esphome.codegen as cg
from esphome.components.const import CONF_MAX_PACKET_SIZE
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@clydebarrow"]
DOMAIN = "packet_interface"
IS_PLATFORM_COMPONENT = True
CONF_PACKET_INTERFACE = "packet_interface"
packet_interface_ns = cg.esphome_ns.namespace("packet_interface")
PacketInterface = packet_interface_ns.class_("PacketInterface", cg.Component)
__all__ = [
"CONF_MAX_PACKET_SIZE",
"CONF_PACKET_INTERFACE",
"PacketInterface",
"new_packet_interface",
"packet_interface_ns",
"packet_interface_schema",
]
def packet_interface_schema(
class_: MockObjClass,
default_max_packet_size: int,
max_packet_size_limit: int = 4096,
) -> cv.Schema:
"""Schema for a link platform."""
return cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.Optional(
CONF_MAX_PACKET_SIZE, default=default_max_packet_size
): cv.int_range(16, max_packet_size_limit),
}
).extend(cv.COMPONENT_SCHEMA)
async def new_packet_interface(config: ConfigType, *args: Any) -> MockObj:
"""Create the link layer.
The link's constructor takes its maximum packet size, usually as a std::span over a buffer that code generation
sized from the configuration, so the two cannot differ.
Returns the link layer; applications reach the top layer with PacketInterface::top().
"""
var = cg.new_Pvariable(config[CONF_ID], *args)
cg.add(var.set_name(config[CONF_ID].id))
await cg.register_component(var, config)
return var
@@ -0,0 +1,173 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include <vector>
namespace esphome::packet_interface {
/**
* A buffer class that efficiently represents a composite buffer with three sections:
* - pre_buffer: A pointer to uint8_t data with a length
* - mid_buffer: A reference to another PacketBuffer (optional)
* - post_buffer: A pointer to uint8_t data with a length
*
* This allows efficient zero-copy composition of buffers without allocating new memory.
*
* WARNING: PacketBuffer holds raw pointers to data. The user must ensure that the data remains valid
* for the lifetime of the PacketBuffer instance. Storing PacketBuffer instances beyond the scope of the data they
* reference will lead to dangling pointers and undefined behavior, so to keep a packet, copy it with copy_to() into a
* buffer allocated in setup().
*/
class PacketBuffer {
public:
PacketBuffer() = default;
// Disable copy semantics - PacketBuffer contains pointers and copying would create dangling references
PacketBuffer(const PacketBuffer &) = delete;
PacketBuffer &operator=(const PacketBuffer &) = delete;
// Enable move semantics
PacketBuffer(PacketBuffer &&) = default;
PacketBuffer &operator=(PacketBuffer &&) = default;
/**
* Construct from a single buffer.
*/
PacketBuffer(const uint8_t *data, size_t length) : pre_buffer_(data), pre_length_(length) {}
/**
* Construct from a vector.
*/
explicit PacketBuffer(const std::vector<uint8_t> &vec) : pre_buffer_(vec.data()), pre_length_(vec.size()) {}
// A temporary vector would leave the buffer dangling
explicit PacketBuffer(std::vector<uint8_t> &&) = delete;
/**
* Construct with pre and post buffers.
*/
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const uint8_t *post_data, size_t post_length)
: pre_buffer_(pre_data), pre_length_(pre_length), post_buffer_(post_data), post_length_(post_length) {}
/**
* Construct with pre, mid, and post buffers.
*/
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const PacketBuffer *mid_buffer, const uint8_t *post_data,
size_t post_length)
: pre_buffer_(pre_data),
pre_length_(pre_length),
mid_buffer_(mid_buffer),
post_buffer_(post_data),
post_length_(post_length) {}
/**
* Get the total size of the buffer.
*/
size_t size() const {
size_t total = this->pre_length_ + this->post_length_;
if (this->mid_buffer_ != nullptr) {
total += this->mid_buffer_->size();
}
return total;
}
/**
* Check if the buffer is empty.
*/
bool empty() const { return this->size() == 0; }
/**
* Check if all the data is held in the pre-buffer, so it can be read as one span.
*/
bool is_contiguous() const { return this->mid_buffer_ == nullptr && this->post_length_ == 0; }
/**
* Get the data as one span. Only valid when is_contiguous() is true.
*/
std::span<const uint8_t> as_span() const { return {this->pre_buffer_, this->pre_length_}; }
/**
* Access a byte at a specific position.
* @param index The position to access
* @return The byte at the specified position, or 0 past the end
*/
uint8_t operator[](size_t index) const {
if (index < this->pre_length_) {
return this->pre_buffer_[index];
}
index -= this->pre_length_;
if (this->mid_buffer_ != nullptr) {
size_t mid_size = this->mid_buffer_->size();
if (index < mid_size) {
return (*this->mid_buffer_)[index];
}
index -= mid_size;
}
return index < this->post_length_ ? this->post_buffer_[index] : 0;
}
/**
* Copy data from the buffer to a destination pointer.
* @param dest Destination pointer
* @param count Number of bytes to copy
* @param offset Offset in the source buffer to start copying from
* @return Number of bytes actually copied
*/
size_t copy_to(uint8_t *dest, size_t count, size_t offset = 0) const {
size_t total_size = this->size();
if (offset >= total_size) {
return 0;
}
size_t available = total_size - offset;
size_t to_copy = count < available ? count : available;
size_t copied = 0;
// Copy from pre_buffer
if (offset < this->pre_length_) {
size_t pre_offset = offset;
size_t pre_available = this->pre_length_ - pre_offset;
size_t pre_copy = to_copy < pre_available ? to_copy : pre_available;
std::memcpy(dest, this->pre_buffer_ + pre_offset, pre_copy);
copied += pre_copy;
dest += pre_copy;
offset += pre_copy;
}
// Copy from mid_buffer
size_t mid_size = this->mid_buffer_ != nullptr ? this->mid_buffer_->size() : 0;
if (copied < to_copy && offset < this->pre_length_ + mid_size) {
size_t mid_offset = offset - this->pre_length_;
size_t mid_available = mid_size - mid_offset;
size_t mid_copy = (to_copy - copied) < mid_available ? (to_copy - copied) : mid_available;
this->mid_buffer_->copy_to(dest, mid_copy, mid_offset);
copied += mid_copy;
dest += mid_copy;
offset += mid_copy;
}
// Copy from post_buffer
if ((copied < to_copy) && (this->post_length_ > 0)) {
size_t post_offset = offset - this->pre_length_ - mid_size;
size_t post_available = this->post_length_ - post_offset;
size_t post_copy = (to_copy - copied) < post_available ? (to_copy - copied) : post_available;
std::memcpy(dest, this->post_buffer_ + post_offset, post_copy);
copied += post_copy;
}
return copied;
}
private:
const uint8_t *pre_buffer_{nullptr};
size_t pre_length_{0};
const PacketBuffer *mid_buffer_{nullptr};
const uint8_t *post_buffer_{nullptr};
size_t post_length_{0};
};
} // namespace esphome::packet_interface
@@ -0,0 +1,39 @@
#include "packet_interface.h"
#include "esphome/core/log.h"
namespace esphome::packet_interface {
ESPHOME_LOG_TAG(TAG, "packet_interface");
// Packets in a row that cannot be understood before the mismatch is logged
static constexpr uint8_t MISMATCH_COUNT = 10;
void PacketInterface::dump_config() {
ESP_LOGCONFIG(TAG,
"Packet Interface '%s':\n"
" Max Packet Size: %zu",
this->name_, this->get_max_packet_size());
}
void PacketInterface::on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
static constexpr size_t LOG_BYTES = 24;
uint8_t head[LOG_BYTES];
size_t len = data.copy_to(head, LOG_BYTES);
ESP_LOGV_HEX(TAG, head, len, LOG_BYTES, "%s: received %zu bytes %s", this->name_, data.size());
#endif
this->callback_.call(data, meta_data);
}
void PacketInterface::note_received_(bool understood) {
if (understood) {
this->not_understood_count_ = 0;
} else if (this->not_understood_count_ < MISMATCH_COUNT) {
this->not_understood_count_++;
if (this->not_understood_count_ == MISMATCH_COUNT) {
ESP_LOGW(TAG, "%s: %u packets in a row could not be understood; check that both ends have the same layers",
this->name_, MISMATCH_COUNT);
}
}
}
} // namespace esphome::packet_interface
@@ -0,0 +1,149 @@
#pragma once
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <utility>
#include "esphome/core/string_ref.h"
#include "esphome/core/helpers.h"
#include "esphome/core/component.h"
#include "packet_buffer.h"
namespace esphome::packet_interface {
/// The room left for a packet of \p size bytes after \p used bytes, or 0 if it is too small.
constexpr size_t remaining_packet_size(size_t size, size_t used) { return size > used ? size - used : 0; }
/// What a stack of layers provides, as bits of PacketInterface::get_capabilities().
enum Capability : uint8_t {
CAPABILITY_ENCRYPTION = 1 << 0,
CAPABILITY_RELIABILITY = 1 << 1,
CAPABILITY_SESSION = 1 << 2,
CAPABILITY_ADDRESSING = 1 << 3,
CAPABILITY_FRAGMENTATION = 1 << 4,
};
/**
* A struct to hold metadata about a packet.
* @param info Additional information about the packet
* @param source_address With an addressing layer, the sender of a received packet; 0 if not known
* @param destination_address With an addressing layer, the destination of a packet; when sending, 0 uses the
* configured destination
* @param rssi The RSSI of the packet
* @param snr The SNR of the packet
* @param mac_address The MAC address of the sender/receiver
*/
class PacketMetaData {
public:
StringRef info{};
uint16_t source_address{0};
uint16_t destination_address{0};
float rssi{NAN};
float snr{NAN};
uint8_t mac_address[MAC_ADDRESS_SIZE]{};
};
/**
* A class that will transport data to and from an abstract channel.
*
* The operations defined are:
*
* Transmit: the function send_to_interface() takes a PacketBuffer of bytes, and returns when all data is
* written or buffered for transmission.
*
* Receive: The callbacks for an instance of this class are called with currently received data.
*/
class PacketInterface : public Component {
public:
/**
*
* Method to be implemented by child classes to send data to the interface.
*
* @param data The data to be sent
* @param meta_data Metadata about the packet to be sent
* @return True if the data was queued for transmission, false if it could not be sent
*/
virtual bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) = 0;
/// The largest packet this layer accepts; fixed when the layer is constructed.
size_t get_max_packet_size() const { return this->max_packet_size_; }
/// How many packets send_to_interface() would accept now, so a layer above can tell before sending a series.
virtual size_t get_send_capacity() const { return SIZE_MAX; }
/// The capabilities of this layer and every layer below it.
uint8_t get_capabilities() const { return this->capabilities_; }
bool has_capability(Capability capability) const { return (this->capabilities_ & capability) != 0; }
/**
* With CAPABILITY_SESSION, the callback is called with true when the peer can be reached, including after it
* restarts, and with false when it is lost. The session layer is always the top layer, so register on top().
* A reliability layer calls it with false when it resets the link and discards data, which the session layer above
* reports as the session lost.
*/
template<typename F> void add_on_session_state_callback(F &&callback) {
this->session_callback_.add(std::forward<F>(callback));
}
/// The topmost layer stacked on this one, or this one if nothing is stacked on it.
PacketInterface *top() {
PacketInterface *layer = this;
while (layer->upper_ != nullptr)
layer = layer->upper_;
return layer;
}
/// Name shown in log messages; a layer takes the name of the link it is stacked on.
void set_name(const char *name) { this->name_ = name; }
const char *get_name() const { return this->name_; }
/**
* Add a listener that will be called when data is received.
*
* @param callback The callback to be called when data is received.
*/
template<typename F> void add_packet_interface_listener(F &&callback) {
this->callback_.add(std::forward<F>(callback));
}
void dump_config() override;
protected:
/// @param max_packet_size The largest packet the layer accepts, set from the layer below or the layer's own buffer
explicit PacketInterface(size_t max_packet_size) : max_packet_size_(max_packet_size) {}
virtual ~PacketInterface() = default;
/**
* This function should be called by implementing classes with received data to be passed to any listeners.
*
* @param data The received data
* @param meta_data Metadata about the received packet
*/
void on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data);
/**
* Called by a layer for each received packet it understood, or could not make sense of. A run of packets that
* cannot be understood usually means the peer has different layers configured, which is logged once per run.
*/
void note_received_(bool understood);
/// Called by a layer's constructor to record itself as the layer above \p lower, adding its own capability.
void stack_on_(PacketInterface *lower, Capability capability) {
lower->upper_ = this;
this->inherit_from_(lower, capability);
}
/// Takes the name and capabilities of `lower` without becoming its top, for one of several stacks on a link.
void inherit_from_(PacketInterface *lower, Capability capability) {
this->name_ = lower->name_;
this->capabilities_ = lower->capabilities_ | capability;
// A link that has a session of its own, such as TCP, reports it to whatever is stacked above, as does a
// reliability layer that discards data; the session layer handles the latter itself
if ((lower->has_capability(CAPABILITY_SESSION) || lower->has_capability(CAPABILITY_RELIABILITY)) &&
capability != CAPABILITY_SESSION) {
lower->add_on_session_state_callback([this](bool connected) { this->session_callback_.call(connected); });
}
}
LazyCallbackManager<void(const PacketBuffer &, const PacketMetaData &)> callback_;
LazyCallbackManager<void(bool)> session_callback_;
PacketInterface *upper_{nullptr};
const char *name_{""};
const size_t max_packet_size_;
uint8_t capabilities_{0};
uint8_t not_understood_count_{0};
};
} // namespace esphome::packet_interface
@@ -0,0 +1,28 @@
import esphome.codegen as cg
from esphome.components.packet_transport import (
PacketTransport,
new_packet_transport,
transport_schema,
)
import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import CONF_PACKET_INTERFACE, PacketInterface, packet_interface_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["packet_interface"]
PacketInterfaceTransport = packet_interface_ns.class_(
"PacketInterfaceTransport", PacketTransport
)
CONFIG_SCHEMA = transport_schema(PacketInterfaceTransport).extend(
{
cv.GenerateID(CONF_PACKET_INTERFACE): cv.use_id(PacketInterface),
}
)
async def to_code(config: ConfigType) -> None:
interface = await cg.get_variable(config[CONF_PACKET_INTERFACE])
await new_packet_transport(config, interface)
@@ -0,0 +1,29 @@
#include "packet_interface_transport.h"
#include "esphome/core/log.h"
namespace esphome::packet_interface {
ESPHOME_LOG_TAG(TAG, "packet_interface.transport");
void PacketInterfaceTransport::setup() {
PacketTransport::setup();
this->interface_ = this->interface_->top();
this->interface_->add_packet_interface_listener(
[this](const PacketBuffer &data, const PacketMetaData &) { this->receive_(data); });
}
void PacketInterfaceTransport::receive_(const PacketBuffer &data) {
// Packet interface layers deliver packets in one piece
if (!data.is_contiguous()) {
ESP_LOGW(TAG, "Packet split across buffers is not supported");
return;
}
this->process_(data.as_span());
}
void PacketInterfaceTransport::send_packet(const std::vector<uint8_t> &buf) const {
if (!this->interface_->send_to_interface(PacketBuffer(buf), {}))
ESP_LOGD(TAG, "Failed to send packet of %zu bytes", buf.size());
}
} // namespace esphome::packet_interface
@@ -0,0 +1,29 @@
#pragma once
#include "esphome/components/packet_interface/packet_interface.h"
#include "esphome/components/packet_transport/packet_transport.h"
namespace esphome::packet_interface {
/**
* A transport protocol adapter that bridges PacketInterface to PacketTransport.
* This allows packet_transport functionality to be used with any PacketInterface implementation.
*/
class PacketInterfaceTransport : public packet_transport::PacketTransport {
public:
explicit PacketInterfaceTransport(PacketInterface *interface) : interface_(interface) {}
void setup() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
protected:
// implementing PacketTransport virtual methods
size_t get_max_packet_size() override { return this->interface_->top()->get_max_packet_size(); }
void send_packet(const std::vector<uint8_t> &buf) const override;
void receive_(const PacketBuffer &data);
PacketInterface *interface_;
};
} // namespace esphome::packet_interface
@@ -205,8 +205,10 @@ async def register_packet_transport(var: MockObj, config: ConfigType) -> set[str
return providers
async def new_packet_transport(config: ConfigType) -> tuple[MockObj, set[str]]:
var = cg.new_Pvariable(config[CONF_ID])
async def new_packet_transport(
config: ConfigType, *args: Any
) -> tuple[MockObj, set[str]]:
var = cg.new_Pvariable(config[CONF_ID], *args)
cg.add(var.set_platform_name(config[CONF_PLATFORM]))
providers = await register_packet_transport(var, config)
return var, providers
@@ -547,9 +547,11 @@ void PacketTransport::dump_config() {
ESP_LOGCONFIG(TAG,
"Packet Transport:\n"
" Platform: %s\n"
" Max Packet Size: %zu\n"
" Encrypted: %s\n"
" Ping-pong: %s",
this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_));
this->platform_name_, this->get_max_packet_size(), YESNO(this->is_encrypted_()),
YESNO(this->ping_pong_enable_));
#ifdef USE_SENSOR
for (const auto &sensor : this->sensors_) {
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id);
@@ -0,0 +1,36 @@
import esphome.codegen as cg
from esphome.components import packet_interface, uart
from esphome.components.packet_interface import (
CONF_MAX_PACKET_SIZE,
packet_interface_schema,
)
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import uart_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["uart"]
UartPacketInterface = uart_ns.class_(
"UartPacketInterface",
packet_interface.PacketInterface,
uart.UARTDevice,
)
CONFIG_SCHEMA = packet_interface_schema(UartPacketInterface, 508).extend(
uart.UART_DEVICE_SCHEMA
)
async def to_code(config: ConfigType) -> None:
# 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 packet_interface.new_packet_interface(
config, cg.RawExpression(rx_buffer)
)
await uart.register_uart_device(var, config)
@@ -0,0 +1,85 @@
#include "esphome/core/log.h"
#include "uart_packet_interface.h"
namespace esphome::uart {
using packet_interface::PacketBuffer;
using packet_interface::PacketMetaData;
ESPHOME_LOG_TAG(TAG, "uart.packet_interface");
static constexpr uint8_t FLAG_BYTE = 0x7E;
static constexpr uint8_t CONTROL_BYTE = 0x7D;
static constexpr uint8_t ESCAPE_XOR = 0x20;
// Raw bytes escaped and written per UART write
static constexpr size_t TX_CHUNK_SIZE = 32;
void UartPacketInterface::loop() {
while (this->available()) {
uint8_t byte;
if (!this->read_byte(&byte)) {
ESP_LOGW(TAG, "Failed to read byte from UART");
return;
}
if (byte == FLAG_BYTE) {
// A flag that ends a frame also starts the next, so an unfinished frame cannot hide the one after it
if (this->rx_started_ && this->rx_length_ != 0)
this->on_receive_from_interface_(PacketBuffer(this->rx_buffer_.data(), this->rx_length_), {});
this->rx_started_ = true;
this->rx_length_ = 0;
this->rx_control_ = false;
continue;
}
if (!this->rx_started_)
continue;
if (byte == CONTROL_BYTE) {
this->rx_control_ = true;
continue;
}
if (this->rx_control_) {
byte ^= ESCAPE_XOR;
this->rx_control_ = false;
}
if (this->rx_length_ >= this->rx_buffer_.size()) {
ESP_LOGD(TAG, "Packet too large, discarding");
this->rx_started_ = false;
this->rx_length_ = 0;
continue;
}
this->rx_buffer_[this->rx_length_++] = byte;
}
}
bool UartPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
size_t size = data.size();
if (size == 0)
return true;
if (size > this->max_packet_size_) {
ESP_LOGW(TAG, "Packet of %zu bytes is larger than %zu", size, this->max_packet_size_);
return false;
}
uint8_t raw[TX_CHUNK_SIZE]{};
// Every byte may be escaped, plus a flag byte at each end
uint8_t out[TX_CHUNK_SIZE * 2 + 2];
size_t out_len = 0;
out[out_len++] = FLAG_BYTE;
for (size_t offset = 0; offset < size;) {
size_t count = data.copy_to(raw, TX_CHUNK_SIZE, offset);
offset += count;
for (size_t i = 0; i != count; i++) {
uint8_t byte = raw[i];
if (byte == FLAG_BYTE || byte == CONTROL_BYTE) {
out[out_len++] = CONTROL_BYTE;
byte ^= ESCAPE_XOR;
}
out[out_len++] = byte;
}
if (offset == size)
out[out_len++] = FLAG_BYTE;
this->write_array(out, out_len);
out_len = 0;
}
return true;
}
} // namespace esphome::uart
@@ -0,0 +1,35 @@
#pragma once
#include <span>
#include "esphome/components/packet_interface/packet_interface.h"
#include "esphome/components/uart/uart.h"
namespace esphome::uart {
/**
* A packet interface for sending and receiving framed packets over a UART connection.
* Uses simple byte-stuffing framing with FLAG_BYTE and CONTROL_BYTE.
* The protocol wraps data between FLAG_BYTEs.
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
* XORed with 0x20.
*/
class UartPacketInterface : public packet_interface::PacketInterface, public UARTDevice {
public:
/// @param rx_buffer Holds the unescaped data being received; its size is the largest packet, and it must outlive
/// the link
explicit UartPacketInterface(std::span<uint8_t> rx_buffer)
: packet_interface::PacketInterface(rx_buffer.size()), rx_buffer_(rx_buffer) {}
void loop() override;
bool send_to_interface(const packet_interface::PacketBuffer &data,
const packet_interface::PacketMetaData &meta_data) override;
protected:
const std::span<uint8_t> rx_buffer_;
size_t rx_length_{0};
bool rx_started_{};
bool rx_control_{};
};
} // namespace esphome::uart
@@ -6,6 +6,10 @@ namespace esphome::uart {
ESPHOME_LOG_TAG(TAG, "uart_transport");
static constexpr uint16_t MAX_PACKET_SIZE = 508;
static constexpr uint8_t FLAG_BYTE = 0x7E;
static constexpr uint8_t CONTROL_BYTE = 0x7D;
void UARTTransport::loop() {
PacketTransport::loop();
@@ -78,4 +82,5 @@ void UARTTransport::send_packet(const std::vector<uint8_t> &buf) const {
this->parent_->write_byte(FLAG_BYTE);
}
size_t UARTTransport::get_max_packet_size() { return MAX_PACKET_SIZE; }
} // namespace esphome::uart
@@ -16,9 +16,6 @@ namespace esphome::uart {
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
* XORed with 0x20.
*/
static const uint16_t MAX_PACKET_SIZE = 508;
static const uint8_t FLAG_BYTE = 0x7E;
static const uint8_t CONTROL_BYTE = 0x7D;
class UARTTransport final : public packet_transport::PacketTransport, public UARTDevice {
public:
@@ -29,7 +26,7 @@ class UARTTransport final : public packet_transport::PacketTransport, public UAR
void write_byte_(uint8_t byte) const;
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; };
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
size_t get_max_packet_size() override;
std::vector<uint8_t> receive_buffer_{};
bool rx_started_{};
bool rx_control_{};
@@ -0,0 +1,40 @@
import esphome.codegen as cg
from esphome.components import packet_interface
from esphome.components.packet_interface import (
CONF_MAX_PACKET_SIZE,
new_packet_interface,
packet_interface_schema,
)
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import CONF_UDP_ID, UDP_SCHEMA, udp_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["udp"]
# Size of the receive buffer of the udp component
MAX_PACKET_SIZE = 508
UdpPacketInterface = udp_ns.class_(
"UdpPacketInterface",
packet_interface.PacketInterface,
)
CONFIG_SCHEMA = packet_interface_schema(
UdpPacketInterface, MAX_PACKET_SIZE, MAX_PACKET_SIZE
).extend(UDP_SCHEMA)
async def to_code(config: ConfigType) -> None:
udp_var = await cg.get_variable(config[CONF_UDP_ID])
cg.add(udp_var.set_should_listen())
# The udp component sends to its addresses through its broadcast socket
cg.add(udp_var.set_should_broadcast())
# The copy buffer's size is known here, so it is a static array rather than allocated at run time
tx_buffer = f"{config[CONF_ID].id}__tx_buffer"
cg.add_global(
cg.RawStatement(f"static uint8_t {tx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];")
)
await new_packet_interface(config, udp_var, cg.RawExpression(tx_buffer))
@@ -0,0 +1,43 @@
#include "udp_packet_interface.h"
#ifdef USE_NETWORK
#include "esphome/components/network/util.h"
#include "esphome/core/log.h"
namespace esphome::udp {
using packet_interface::PacketBuffer;
using packet_interface::PacketMetaData;
ESPHOME_LOG_TAG(TAG, "udp.packet_interface");
void UdpPacketInterface::setup() {
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->receive_(data); });
}
void UdpPacketInterface::receive_(std::span<const uint8_t> data) {
if (data.size() > this->max_packet_size_) {
ESP_LOGV(TAG, "%s: Packet of %zu bytes is larger than %zu, dropped", this->name_, data.size(),
this->max_packet_size_);
return;
}
this->on_receive_from_interface_(PacketBuffer(data.data(), data.size()), {});
}
bool UdpPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
size_t size = data.size();
if (size > this->max_packet_size_) {
ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
return false;
}
if (!network::is_connected())
return false;
if (data.is_contiguous()) {
auto span = data.as_span();
return this->parent_->send_packet(span.data(), span.size());
}
data.copy_to(this->tx_buffer_.data(), size);
return this->parent_->send_packet(this->tx_buffer_.data(), size);
}
} // namespace esphome::udp
#endif
@@ -0,0 +1,37 @@
#pragma once
#include "esphome/core/defines.h"
// udp_component.h declares nothing without USE_NETWORK
#ifdef USE_NETWORK
#include <span>
#include "esphome/components/packet_interface/packet_interface.h"
#include "../udp_component.h"
namespace esphome::udp {
/**
* A packet interface link over UDP: each packet is one datagram, sent to every address of the UDP component.
*
* Every datagram received on the listen port is passed up, from any sender, so on a shared network an addressing
* or encryption layer is needed to tell the peer's packets from others.
*/
class UdpPacketInterface : public packet_interface::PacketInterface {
public:
/// @param tx_buffer Joins packets split across buffers; its size is the largest packet, and it must outlive the link
UdpPacketInterface(UDPComponent *udp_component, std::span<uint8_t> tx_buffer)
: packet_interface::PacketInterface(tx_buffer.size()), parent_(udp_component), tx_buffer_(tx_buffer) {}
void setup() override;
bool send_to_interface(const packet_interface::PacketBuffer &data,
const packet_interface::PacketMetaData &meta_data) override;
protected:
void receive_(std::span<const uint8_t> data);
UDPComponent *parent_;
const std::span<uint8_t> tx_buffer_;
};
} // namespace esphome::udp
#endif
@@ -1,12 +1,9 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/network/util.h"
#include "udp_transport.h"
namespace esphome::udp {
ESPHOME_LOG_TAG(TAG, "udp_transport");
bool UDPTransport::should_send() { return network::is_connected(); }
void UDPTransport::setup() {
PacketTransport::setup();
+34 -14
View File
@@ -205,18 +205,25 @@ void UDPComponent::set_poll_warning_(PollStep step, const LogString *what) {
}
void UDPComponent::loop() {
if (this->should_listen_) {
std::array<uint8_t, MAX_PACKET_SIZE> buf;
for (;;) {
if (!this->listen_socket_->ready())
break;
auto len = this->listen_socket_->read(buf.data(), buf.size());
if (len <= 0)
break;
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));
if (!this->should_listen_) {
this->disable_loop();
return;
}
// One byte more than the largest packet, as a read cuts a longer datagram to fit and so cannot report it
std::array<uint8_t, MAX_PACKET_SIZE + 1> buf;
for (;;) {
if (!this->listen_socket_->ready())
break;
auto len = this->listen_socket_->read(buf.data(), buf.size());
if (len <= 0)
break;
size_t packet_len = static_cast<size_t>(len);
if (packet_len > MAX_PACKET_SIZE) {
ESP_LOGV(TAG, "Packet longer than %zu bytes dropped", MAX_PACKET_SIZE);
continue;
}
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
}
}
@@ -239,9 +246,10 @@ void UDPComponent::dump_config() {
YESNO(this->should_broadcast_), YESNO(this->should_listen_));
}
void UDPComponent::send_packet(const uint8_t *data, size_t size) {
bool UDPComponent::send_packet(const uint8_t *data, size_t size) {
if (this->is_failed())
return; // a send socket may be missing
return false; // a send socket may be missing
bool sent = true;
for (const auto &entry : this->sockaddrs_) {
#ifdef USE_UDP_IPV6_SEND
const bool v6 = entry.sa.sa_family == AF_INET6;
@@ -256,11 +264,23 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
socket::UDPSendSocket *sock = this->broadcast_socket_.get();
socklen_t sockaddr_size = sizeof(sockaddr_in);
#endif
if (sock == nullptr) {
ESP_LOGW(TAG, "Broadcast socket not initialized");
return false;
}
auto result = sock->sendto(data, size, 0, &entry.sa, sockaddr_size);
if (result < 0) {
ESP_LOGW(TAG, "sendto() error %d", errno);
// lwIP refuses a packet with ENOMEM while the destination's ARP lookup is pending and its queue is full;
// the packet can be sent again later
if (errno == ENOMEM) {
ESP_LOGV(TAG, "sendto() out of memory");
} else {
ESP_LOGW(TAG, "sendto() error %d", errno);
}
sent = false;
}
}
return sent;
}
} // namespace esphome::udp
+4 -3
View File
@@ -35,8 +35,9 @@ class UDPComponent final : public Component {
void setup() override;
void loop() override;
void dump_config() override;
void send_packet(const uint8_t *data, size_t size);
void send_packet(const std::vector<uint8_t> &buf) { this->send_packet(buf.data(), buf.size()); }
/// Returns false if the packet could not be sent to every destination.
bool send_packet(const uint8_t *data, size_t size);
bool send_packet(const std::vector<uint8_t> &buf) { return this->send_packet(buf.data(), buf.size()); }
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; };
protected:
@@ -53,7 +54,7 @@ class UDPComponent final : public Component {
bool network_poll_();
void set_poll_warning_(PollStep step, const LogString *what);
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
LazyCallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
std::unique_ptr<socket::UDPSendSocket> broadcast_socket_ = nullptr;
std::unique_ptr<socket::UDPSocket> listen_socket_ = nullptr;
#ifdef USE_UDP_IPV6_SEND
+5
View File
@@ -188,6 +188,8 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict:
The ``esphome:`` key is special: its sub-keys are merged into the
existing esphome config (e.g. to add ``areas:`` or ``devices:``).
A list, such as the entries of a platform component, is extended, so
several directories can each add their own entries to one key.
Keys already present in the base config (e.g. ``host:``, ``logger:``)
are protected by ``setdefault`` in the caller.
@@ -213,6 +215,9 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict:
for sub_key, sub_value in value.items():
esphome_extra.setdefault(sub_key, sub_value)
continue
if isinstance(value, list) and isinstance(merged.get(key), list):
merged[key] = merged[key] + value
continue
merged.setdefault(key, value)
return merged
@@ -0,0 +1,11 @@
uart:
port: /dev/ttyUSB0
baud_rate: 9600
packet_interface:
- platform: uart
id: bridge_test_link
packet_transport:
- platform: packet_interface
packet_interface: bridge_test_link
@@ -0,0 +1,29 @@
wifi:
ssid: MySSID
password: password1
udp:
- id: packet_interface_udp
packet_interface:
- platform: uart
id: packet_interface_uart
max_packet_size: 256
- platform: udp
id: packet_interface_network
udp_id: packet_interface_udp
sensor:
- platform: template
id: packet_interface_sensor
lambda: return 1.0;
packet_transport:
- platform: packet_interface
packet_interface: packet_interface_uart
sensors:
- packet_interface_sensor
- platform: packet_interface
packet_interface: packet_interface_network
sensors:
- packet_interface_sensor
@@ -0,0 +1,238 @@
#include <gtest/gtest.h>
#include <vector>
#include <cstdint>
#include "esphome/components/packet_interface/packet_buffer.h"
namespace esphome::packet_interface::testing {
// Copies a packet into a vector, for comparing in tests
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
std::vector<uint8_t> result(data.size());
data.copy_to(result.data(), result.size());
return result;
}
// Test default constructor
TEST(PacketBufferTest, DefaultConstructor) {
PacketBuffer buffer;
EXPECT_EQ(buffer.size(), 0);
EXPECT_TRUE(buffer.empty());
}
// Test single buffer constructor
TEST(PacketBufferTest, SingleBufferConstructor) {
uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
PacketBuffer buffer(data, 4);
EXPECT_EQ(buffer.size(), 4);
EXPECT_FALSE(buffer.empty());
EXPECT_EQ(buffer[0], 0x01);
EXPECT_EQ(buffer[3], 0x04);
}
// Test vector constructor
TEST(PacketBufferTest, VectorConstructor) {
std::vector<uint8_t> vec = {0x10, 0x20, 0x30};
PacketBuffer buffer(vec);
EXPECT_EQ(buffer.size(), 3);
EXPECT_EQ(buffer[0], 0x10);
EXPECT_EQ(buffer[1], 0x20);
EXPECT_EQ(buffer[2], 0x30);
}
// Test pre and post buffer constructor
TEST(PacketBufferTest, PrePostConstructor) {
uint8_t pre[] = {0x01, 0x02};
uint8_t post[] = {0x03, 0x04, 0x05};
PacketBuffer buffer(pre, 2, post, 3);
EXPECT_EQ(buffer.size(), 5);
EXPECT_EQ(buffer[0], 0x01);
EXPECT_EQ(buffer[1], 0x02);
EXPECT_EQ(buffer[2], 0x03);
EXPECT_EQ(buffer[3], 0x04);
EXPECT_EQ(buffer[4], 0x05);
}
// Test pre, mid, and post buffer constructor
TEST(PacketBufferTest, PreMidPostConstructor) {
uint8_t pre[] = {0x01};
std::vector<uint8_t> mid_vec = {0x02, 0x03, 0x04};
PacketBuffer mid_buffer(mid_vec);
uint8_t post[] = {0x05, 0x06};
PacketBuffer buffer(pre, 1, &mid_buffer, post, 2);
EXPECT_EQ(buffer.size(), 6);
EXPECT_EQ(buffer[0], 0x01);
EXPECT_EQ(buffer[1], 0x02);
EXPECT_EQ(buffer[2], 0x03);
EXPECT_EQ(buffer[3], 0x04);
EXPECT_EQ(buffer[4], 0x05);
EXPECT_EQ(buffer[5], 0x06);
}
// Test nested mid buffers
TEST(PacketBufferTest, NestedMidBuffers) {
uint8_t data1[] = {0x01};
uint8_t data2[] = {0x02, 0x03};
uint8_t data3[] = {0x04};
PacketBuffer inner(data2, 2);
PacketBuffer middle(data1, 1, &inner, data3, 1);
uint8_t pre[] = {0x00};
uint8_t post[] = {0x05};
PacketBuffer outer(pre, 1, &middle, post, 1);
EXPECT_EQ(outer.size(), 6);
EXPECT_EQ(outer[0], 0x00);
EXPECT_EQ(outer[1], 0x01);
EXPECT_EQ(outer[2], 0x02);
EXPECT_EQ(outer[3], 0x03);
EXPECT_EQ(outer[4], 0x04);
EXPECT_EQ(outer[5], 0x05);
}
// Test indexing operator
TEST(PacketBufferTest, IndexingOperator) {
uint8_t data[] = {0xAA, 0xBB, 0xCC, 0xDD};
PacketBuffer buffer(data, 4);
EXPECT_EQ(buffer[0], 0xAA);
EXPECT_EQ(buffer[1], 0xBB);
EXPECT_EQ(buffer[2], 0xCC);
EXPECT_EQ(buffer[3], 0xDD);
}
// Test copy_to method
TEST(PacketBufferTest, CopyTo) {
uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
PacketBuffer buffer(data, 5);
uint8_t dest[5] = {0};
size_t copied = buffer.copy_to(dest, 5);
EXPECT_EQ(copied, 5);
for (int i = 0; i < 5; i++) {
EXPECT_EQ(dest[i], data[i]);
}
}
// Test copy_to with offset
TEST(PacketBufferTest, CopyToWithOffset) {
uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
PacketBuffer buffer(data, 5);
uint8_t dest[3] = {0};
size_t copied = buffer.copy_to(dest, 3, 2); // Copy 3 bytes starting from offset 2
EXPECT_EQ(copied, 3);
EXPECT_EQ(dest[0], 0x03);
EXPECT_EQ(dest[1], 0x04);
EXPECT_EQ(dest[2], 0x05);
}
// Test copy_to with partial copy
TEST(PacketBufferTest, CopyToPartial) {
uint8_t data[] = {0x01, 0x02, 0x03};
PacketBuffer buffer(data, 3);
uint8_t dest[5] = {0};
size_t copied = buffer.copy_to(dest, 5); // Request more than available
EXPECT_EQ(copied, 3); // Only 3 bytes copied
}
// Test copy_to from composite buffer
TEST(PacketBufferTest, CopyToComposite) {
uint8_t pre[] = {0x01, 0x02};
uint8_t mid[] = {0x03, 0x04, 0x05};
uint8_t post[] = {0x06};
PacketBuffer mid_buffer(mid, 3);
PacketBuffer buffer(pre, 2, &mid_buffer, post, 1);
uint8_t dest[6] = {0};
size_t copied = buffer.copy_to(dest, 6);
EXPECT_EQ(copied, 6);
EXPECT_EQ(dest[0], 0x01);
EXPECT_EQ(dest[1], 0x02);
EXPECT_EQ(dest[2], 0x03);
EXPECT_EQ(dest[3], 0x04);
EXPECT_EQ(dest[4], 0x05);
EXPECT_EQ(dest[5], 0x06);
}
// Test copying from an offset past the mid buffer
TEST(PacketBufferTest, CopyToOffsetPastMid) {
uint8_t pre[] = {0x01, 0x02};
uint8_t mid_data[] = {0x03};
uint8_t post[] = {0x04, 0x05, 0x06};
PacketBuffer mid(mid_data, 1);
PacketBuffer buffer(pre, 2, &mid, post, 3);
uint8_t dest[4]{};
ASSERT_EQ(buffer.copy_to(dest, 4, 4), 2);
EXPECT_EQ(dest[0], 0x05);
EXPECT_EQ(dest[1], 0x06);
}
// Test single span access
TEST(PacketBufferTest, ContiguousSpan) {
uint8_t data[] = {0x10, 0x20, 0x30, 0x40};
PacketBuffer buffer(data, 4);
ASSERT_TRUE(buffer.is_contiguous());
auto span = buffer.as_span();
EXPECT_EQ(span.data(), data);
EXPECT_EQ(span.size(), 4);
uint8_t post[] = {0x50};
PacketBuffer split(data, 4, post, 1);
EXPECT_FALSE(split.is_contiguous());
PacketBuffer nested(nullptr, 0, &buffer, nullptr, 0);
EXPECT_FALSE(nested.is_contiguous());
}
// Test indexing past the end
TEST(PacketBufferTest, IndexPastEnd) {
uint8_t pre[] = {0x01};
uint8_t post[] = {0x02};
PacketBuffer buffer(pre, 1, post, 1);
EXPECT_EQ(buffer[1], 0x02);
EXPECT_EQ(buffer[2], 0);
EXPECT_EQ(PacketBuffer()[0], 0);
}
// Test empty buffer operations
TEST(PacketBufferTest, EmptyBuffer) {
PacketBuffer buffer;
EXPECT_TRUE(buffer.empty());
EXPECT_EQ(buffer.size(), 0);
uint8_t dest[10] = {0xFF};
size_t copied = buffer.copy_to(dest, 10);
EXPECT_EQ(copied, 0);
EXPECT_EQ(dest[0], 0xFF); // Unchanged
std::vector<uint8_t> vec = to_vector(buffer);
EXPECT_EQ(vec.size(), 0);
}
// Test large buffer
TEST(PacketBufferTest, LargeBuffer) {
std::vector<uint8_t> large_vec(1000);
for (size_t i = 0; i < large_vec.size(); i++) {
large_vec[i] = static_cast<uint8_t>(i % 256);
}
PacketBuffer buffer(large_vec);
EXPECT_EQ(buffer.size(), 1000);
EXPECT_EQ(buffer[0], 0);
EXPECT_EQ(buffer[255], 255);
EXPECT_EQ(buffer[256], 0);
EXPECT_EQ(buffer[999], 231); // 999 % 256 = 231
}
} // namespace esphome::packet_interface::testing
@@ -0,0 +1,92 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include <vector>
#include "esphome/components/packet_interface/packet_transport/packet_interface_transport.h"
#include "esphome/core/application.h"
namespace esphome::packet_interface::transport_testing {
using Packet = std::vector<uint8_t>;
// A link layer that records what is sent, and passes injected packets to its listeners
class FakeInterface : public PacketInterface {
public:
explicit FakeInterface(size_t max_packet_size = 512) : PacketInterface(max_packet_size) {}
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
Packet packet(data.size());
data.copy_to(packet.data(), packet.size());
this->sent.push_back(packet);
return true;
}
void deliver(const PacketBuffer &data) { this->on_receive_from_interface_(data, {}); }
std::vector<Packet> sent;
};
// A layer stacked on a link, which the bridge must use in place of the link
class FakeLayer : public FakeInterface {
public:
explicit FakeLayer(PacketInterface *lower) : FakeInterface(lower->get_max_packet_size()) {
this->stack_on_(lower, CAPABILITY_ENCRYPTION);
}
};
class TestBridge : public PacketInterfaceTransport {
public:
using PacketInterfaceTransport::PacketInterfaceTransport;
using PacketInterfaceTransport::send_packet;
using packet_transport::PacketTransport::ping_keys_;
};
// A packet_transport ping packet; with an encryption key set, process_() stores the sender's key
static Packet ping_packet(const char *hostname, uint32_t key) {
Packet packet{0x48, 0x50, static_cast<uint8_t>(strlen(hostname))};
packet.insert(packet.end(), hostname, hostname + strlen(hostname));
for (int shift = 0; shift != 32; shift += 8)
packet.push_back(static_cast<uint8_t>(key >> shift));
return packet;
}
// Each test replaces App with a fresh one, which the transport's setup() reads the device name from
class PacketInterfaceTransportTest : public ::testing::Test {
protected:
void SetUp() override {
App.~Application();
new (&App) Application();
static char name[] = "bridge-tests";
static char friendly_name[] = "";
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
this->bridge_.set_encryption_key(std::vector<uint8_t>(32, 0xAA));
this->bridge_.setup();
}
void TearDown() override {
App.~Application();
new (&App) Application(); // later tests and the test listener use App
}
FakeInterface link_;
FakeLayer layer_{&this->link_};
TestBridge bridge_{&this->link_};
};
TEST_F(PacketInterfaceTransportTest, SendsThroughTopOfStack) {
this->bridge_.send_packet({1, 2, 3});
EXPECT_EQ(this->layer_.sent, (std::vector<Packet>{{1, 2, 3}}));
EXPECT_TRUE(this->link_.sent.empty());
}
TEST_F(PacketInterfaceTransportTest, PacketInOneBufferReachesTransport) {
Packet ping = ping_packet("requester", 0xDEADBEEF);
this->layer_.deliver(PacketBuffer(ping));
ASSERT_EQ(this->bridge_.ping_keys_.size(), 1u);
EXPECT_EQ(this->bridge_.ping_keys_["requester"], 0xDEADBEEFu);
}
TEST_F(PacketInterfaceTransportTest, PacketSplitAcrossBuffersDropped) {
Packet ping = ping_packet("requester", 0xDEADBEEF);
this->layer_.deliver(PacketBuffer(ping.data(), 4, ping.data() + 4, ping.size() - 4));
EXPECT_TRUE(this->bridge_.ping_keys_.empty());
}
} // namespace esphome::packet_interface::transport_testing
@@ -0,0 +1,3 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
packet_interface: !include common.yaml
@@ -3,12 +3,14 @@ wifi:
password: password1
udp:
listen_address: 239.0.60.53
addresses: ["239.0.60.53"]
- id: transport_udp_component
listen_address: 239.0.60.53
addresses: ["239.0.60.53"]
packet_transport:
- platform: udp
id: transport_udp
udp_id: transport_udp_component
update_interval: 5s
encryption: "our key goes here"
rolling_code_enable: true
+6
View File
@@ -0,0 +1,6 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# benchmark.yaml configures a uart packet_interface, whose to_code waits on the uart bus variable.
manifest.enable_codegen()
+6
View File
@@ -0,0 +1,6 @@
packet_interface:
- platform: uart
uart:
port: /dev/ttyUSB0
baud_rate: 9600
+4 -1
View File
@@ -25,7 +25,10 @@ class MockUARTComponent : public UARTComponent {
// consider using StaticVector or FixedVector from esphome/core/helpers.h instead.
std::vector<uint8_t> written_data;
void write_array(const uint8_t *data, size_t len) override { written_data.assign(data, data + len); }
void write_array(const uint8_t *data, size_t len) override {
written_data.insert(written_data.end(), data, data + len);
}
void clear() { written_data.clear(); }
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
@@ -0,0 +1,392 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <vector>
#include <cstdint>
#include "esphome/components/uart/packet_interface/uart_packet_interface.h"
#include "esphome/components/packet_interface/packet_buffer.h"
#include "esphome/components/uart/uart_component.h"
#include "common.h"
namespace esphome::uart::testing {
// Copies a packet into a vector, for comparing in tests
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
std::vector<uint8_t> result(data.size());
data.copy_to(result.data(), result.size());
return result;
}
using packet_interface::PacketBuffer;
using packet_interface::PacketMetaData;
// Test fixture
class UartPacketInterfaceTest : public ::testing::Test {
protected:
MockUARTComponent uart_{};
// As large as the default max_packet_size; tests only make it smaller
uint8_t rx_buffer_[512]{};
UartPacketInterface interface_{this->rx_buffer_};
std::vector<std::vector<uint8_t>> received_packets_;
void SetUp() override {
received_packets_.clear();
interface_.set_uart_parent(&uart_);
interface_.setup();
interface_.add_packet_interface_listener(
[this](const PacketBuffer &data, const PacketMetaData &meta) { received_packets_.push_back(to_vector(data)); });
}
};
// Feed bytes to the interface through the mocked UART and run its loop
static void feed(MockUARTComponent &uart, UartPacketInterface &interface, const std::vector<uint8_t> &bytes) {
size_t read_pos = 0;
EXPECT_CALL(uart, available()).WillRepeatedly(Invoke([&bytes, &read_pos]() { return bytes.size() - read_pos; }));
EXPECT_CALL(uart, read_array(_, 1)).WillRepeatedly(Invoke([&bytes, &read_pos](uint8_t *data, size_t) {
if (read_pos >= bytes.size())
return false;
*data = bytes[read_pos++];
return true;
}));
interface.loop();
}
// Test basic frame sending
TEST_F(UartPacketInterfaceTest, SendSimpleFrame) {
std::vector<uint8_t> data = {0x01, 0x02, 0x03};
PacketBuffer buffer(data);
// Set up the mock to accumulate bytes
ASSERT_EQ(uart_.written_data.size(), 0);
interface_.send_to_interface(buffer, {});
// Verify the accumulated bytes
ASSERT_EQ(uart_.written_data.size(), 5);
std::vector<uint8_t> expected = {0x7E, 0x01, 0x02, 0x03, 0x7E};
EXPECT_EQ(uart_.written_data, expected);
}
// Test byte stuffing for FLAG_BYTE
TEST_F(UartPacketInterfaceTest, SendWithFlagByteStuffing) {
std::vector<uint8_t> data = {0x01, 0x7E, 0x03};
PacketBuffer buffer(data);
uart_.clear();
interface_.send_to_interface(buffer, {});
// Should escape 0x7E as 0x7D 0x5E
std::vector<uint8_t> expected = {0x7E, 0x01, 0x7D, 0x5E, 0x03, 0x7E};
EXPECT_EQ(uart_.written_data, expected);
}
// Test byte stuffing for CONTROL_BYTE
TEST_F(UartPacketInterfaceTest, SendWithControlByteStuffing) {
std::vector<uint8_t> data = {0x01, 0x7D, 0x03};
PacketBuffer buffer(data);
uart_.clear();
interface_.send_to_interface(buffer, {});
// Should escape 0x7D as 0x7D 0x5D
std::vector<uint8_t> expected = {0x7E, 0x01, 0x7D, 0x5D, 0x03, 0x7E};
EXPECT_EQ(uart_.written_data, expected);
}
// Test sending empty frame
TEST_F(UartPacketInterfaceTest, SendEmptyFrame) {
std::vector<uint8_t> data = {};
PacketBuffer buffer(data);
interface_.send_to_interface(buffer, {});
// Empty data should return without sending
EXPECT_EQ(uart_.written_data.size(), 0);
}
// Test receiving simple frame
TEST_F(UartPacketInterfaceTest, ReceiveSimpleFrame) {
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x02, 0x03, 0x7E};
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
interface_.loop();
ASSERT_EQ(received_packets_.size(), 1);
std::vector<uint8_t> expected = {0x01, 0x02, 0x03};
EXPECT_EQ(received_packets_[0], expected);
}
// Test receiving frame with FLAG_BYTE escape
TEST_F(UartPacketInterfaceTest, ReceiveFrameWithFlagEscape) {
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x7D, 0x5E, 0x03, 0x7E};
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
interface_.loop();
ASSERT_EQ(received_packets_.size(), 1);
std::vector<uint8_t> expected = {0x01, 0x7E, 0x03}; // 0x7E unescaped
EXPECT_EQ(received_packets_[0], expected);
}
// Test receiving frame with CONTROL_BYTE escape
TEST_F(UartPacketInterfaceTest, ReceiveFrameWithControlEscape) {
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x7D, 0x5D, 0x03, 0x7E};
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
interface_.loop();
ASSERT_EQ(received_packets_.size(), 1);
std::vector<uint8_t> expected = {0x01, 0x7D, 0x03}; // 0x7D unescaped
EXPECT_EQ(received_packets_[0], expected);
}
// Test receiving multiple frames
TEST_F(UartPacketInterfaceTest, ReceiveMultipleFrames) {
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x02, 0x7E, 0x7E, 0x03, 0x04, 0x7E};
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
interface_.loop();
ASSERT_EQ(received_packets_.size(), 2) << "Expected 2 packets, got " << received_packets_.size();
std::vector<uint8_t> expected1 = {0x01, 0x02};
std::vector<uint8_t> expected2 = {0x03, 0x04};
auto pkt0 = received_packets_[0];
auto pkt1 = received_packets_[1];
EXPECT_EQ(pkt0, expected1) << "Packet 0 size: " << pkt0.size();
EXPECT_EQ(pkt1, expected2) << "Packet 1 size: " << pkt1.size();
}
// Test receiving empty frame (back-to-back flags)
TEST_F(UartPacketInterfaceTest, ReceiveEmptyFrame) {
std::vector<uint8_t> frame_data = {0x7E, 0x7E};
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
interface_.loop();
// Empty frames should not be delivered
EXPECT_EQ(received_packets_.size(), 0);
}
// Test data before first flag is ignored
TEST_F(UartPacketInterfaceTest, IgnoreDataBeforeFirstFlag) {
std::vector<uint8_t> frame_data = {0xFF, 0xFF, 0x7E, 0x01, 0x02, 0x7E};
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
interface_.loop();
ASSERT_EQ(received_packets_.size(), 1);
std::vector<uint8_t> expected = {0x01, 0x02};
EXPECT_EQ(received_packets_[0], expected);
}
// Test oversized packet is discarded
TEST_F(UartPacketInterfaceTest, OversizedPacketDiscarded) {
uint8_t small_buffer[10]{};
UartPacketInterface interface(small_buffer);
interface.set_uart_parent(&this->uart_);
std::vector<std::vector<uint8_t>> received;
interface.add_packet_interface_listener(
[&received](const PacketBuffer &data, const PacketMetaData &) { received.push_back(to_vector(data)); });
// Create a packet larger than buffer
std::vector<uint8_t> frame_data = {0x7E};
for (int i = 0; i < 15; i++) {
frame_data.push_back(0xAA);
}
frame_data.push_back(0x7E);
feed(this->uart_, interface, frame_data);
// Oversized packet should be discarded
EXPECT_TRUE(received.empty());
}
// Test round-trip (send and receive)
TEST_F(UartPacketInterfaceTest, RoundTrip) {
std::vector<uint8_t> original = {0x01, 0x7E, 0x7D, 0x03};
PacketBuffer send_buffer(original);
// Send
interface_.send_to_interface(send_buffer, {});
// Prepare to receive - use the written data as frame data
std::vector<uint8_t> frame_data = uart_.written_data;
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
// Receive
interface_.loop();
ASSERT_EQ(received_packets_.size(), 1);
EXPECT_EQ(received_packets_[0], original);
}
// Test all bytes can be transmitted (0x00 to 0xFF)
TEST_F(UartPacketInterfaceTest, AllByteValues) {
std::vector<uint8_t> data;
data.reserve(256);
for (int i = 0; i != 256; i++) {
data.push_back(static_cast<uint8_t>(i));
}
PacketBuffer send_buffer(data);
// Send
interface_.send_to_interface(send_buffer, {});
// Prepare to receive - use the written data as frame data
std::vector<uint8_t> frame_data = uart_.written_data;
size_t read_pos = 0;
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
return frame_data.size() - read_pos;
}));
EXPECT_CALL(uart_, read_array(_, 1)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t) {
if (read_pos >= frame_data.size())
return false;
*data = frame_data[read_pos++];
return true;
}));
// Receive
interface_.loop();
ASSERT_EQ(received_packets_.size(), 1);
EXPECT_EQ(received_packets_[0], data);
}
// The buffer passed to the constructor is the largest packet, for receiving and sending alike
TEST_F(UartPacketInterfaceTest, ReceiveLimitedToBuffer) {
uint8_t small_buffer[4]{};
UartPacketInterface interface(small_buffer);
interface.set_uart_parent(&this->uart_);
std::vector<std::vector<uint8_t>> received;
interface.add_packet_interface_listener(
[&received](const PacketBuffer &data, const PacketMetaData &) { received.push_back(to_vector(data)); });
// The first packet is longer than the buffer and is dropped; the next one fits
feed(this->uart_, interface, {0x7E, 1, 2, 3, 4, 5, 6, 0x7E, 7, 8, 0x7E});
EXPECT_EQ(received, (std::vector<std::vector<uint8_t>>{{7, 8}}));
}
// Test a packet larger than max_packet_size is not sent
TEST_F(UartPacketInterfaceTest, OversizedPacketNotSent) {
uint8_t small_buffer[4]{};
UartPacketInterface interface(small_buffer);
interface.set_uart_parent(&this->uart_);
std::vector<uint8_t> data = {0x01, 0x02, 0x03, 0x04, 0x05};
PacketBuffer buffer(data);
EXPECT_FALSE(interface.send_to_interface(buffer, {}));
EXPECT_EQ(uart_.written_data.size(), 0);
}
// Test a composite buffer longer than one write chunk is framed correctly
TEST_F(UartPacketInterfaceTest, SendCompositeAcrossChunks) {
std::vector<uint8_t> pre(40, 0x7E);
std::vector<uint8_t> mid_data = {0x11, 0x22};
std::vector<uint8_t> post(30, 0x33);
PacketBuffer mid(mid_data);
PacketBuffer buffer(pre.data(), pre.size(), &mid, post.data(), post.size());
ASSERT_TRUE(interface_.send_to_interface(buffer, {}));
std::vector<uint8_t> expected = {0x7E};
for (size_t i = 0; i != pre.size(); i++) {
expected.push_back(0x7D);
expected.push_back(0x5E);
}
expected.insert(expected.end(), mid_data.begin(), mid_data.end());
expected.insert(expected.end(), post.begin(), post.end());
expected.push_back(0x7E);
EXPECT_EQ(uart_.written_data, expected);
}
// Test one flag between frames both ends the first and starts the second
TEST_F(UartPacketInterfaceTest, SharedFlagBetweenFrames) {
feed(uart_, interface_, {0x7E, 0x01, 0x7E, 0x02, 0x03, 0x7E});
EXPECT_EQ(received_packets_, (std::vector<std::vector<uint8_t>>{{0x01}, {0x02, 0x03}}));
}
// Test an unfinished frame does not hide the frame after it
TEST_F(UartPacketInterfaceTest, UnfinishedFrameDoesNotHideNext) {
feed(uart_, interface_, {0x7E, 0xAA, 0x7E, 0x01, 0x02, 0x7E});
EXPECT_EQ(received_packets_, (std::vector<std::vector<uint8_t>>{{0xAA}, {0x01, 0x02}}));
}
} // namespace esphome::uart::testing
+6
View File
@@ -0,0 +1,6 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# benchmark.yaml configures a udp packet_interface, whose to_code waits on the udp component variable.
manifest.enable_codegen()
+5
View File
@@ -0,0 +1,5 @@
udp:
addresses: ["192.168.1.255"]
packet_interface:
- platform: udp
@@ -0,0 +1,78 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "esphome/components/udp/packet_interface/udp_packet_interface.h"
#ifdef USE_NETWORK
namespace esphome::udp::testing {
// Copies a packet into a vector, for comparing in tests
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
std::vector<uint8_t> result(data.size());
data.copy_to(result.data(), result.size());
return result;
}
using packet_interface::PacketBuffer;
using packet_interface::PacketMetaData;
using Packet = std::vector<uint8_t>;
// The copy buffer is a base, so it is set up before the link's constructor sees it
struct UdpBufferStorage {
uint8_t tx_buffer_storage[8]{};
};
class TestUdp : public UdpBufferStorage, public UdpPacketInterface {
public:
explicit TestUdp(UDPComponent *udp) : UdpPacketInterface(udp, this->tx_buffer_storage) {
this->add_packet_interface_listener(
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(to_vector(data)); });
}
using UdpPacketInterface::receive_;
std::vector<Packet> received;
};
// The udp component is not set up, so it has no destinations and a send does nothing
class UdpPacketInterfaceTest : public ::testing::Test {
protected:
UDPComponent udp_;
TestUdp link_{&this->udp_};
};
TEST_F(UdpPacketInterfaceTest, ReceivedPacketsUpToMaxPassedUp) {
Packet packet{1, 2, 3, 4, 5, 6, 7, 8};
this->link_.receive_(packet);
Packet oversized{1, 2, 3, 4, 5, 6, 7, 8, 9};
this->link_.receive_(oversized);
EXPECT_EQ(this->link_.received, std::vector<Packet>{packet});
}
TEST_F(UdpPacketInterfaceTest, SendsContiguousAndSplitPackets) {
this->link_.setup();
Packet packet{1, 2, 3, 4};
EXPECT_TRUE(this->link_.send_to_interface(PacketBuffer(packet), {}));
uint8_t header = 0;
PacketBuffer body(packet);
EXPECT_TRUE(this->link_.send_to_interface(PacketBuffer(&header, 1, &body, nullptr, 0), {}));
// A packet split across buffers is joined in the copy buffer before it is sent
const auto *storage = this->link_.tx_buffer_storage;
EXPECT_EQ(Packet(storage, storage + 5), (Packet{0, 1, 2, 3, 4}));
}
TEST_F(UdpPacketInterfaceTest, OversizedPacketNotSent) {
this->link_.setup();
Packet packet{1, 2, 3, 4, 5, 6, 7, 8, 9};
EXPECT_FALSE(this->link_.send_to_interface(PacketBuffer(packet), {}));
uint8_t header = 0;
Packet body_data{1, 2, 3, 4, 5, 6, 7, 8};
PacketBuffer body(body_data);
EXPECT_FALSE(this->link_.send_to_interface(PacketBuffer(&header, 1, &body, nullptr, 0), {}));
}
} // namespace esphome::udp::testing
#endif
+16
View File
@@ -74,3 +74,19 @@ def test_populate_dependency_config_populates_defaults() -> None:
register_platform_fn=lambda domain: None,
)
assert config["ok"] == {"default": 1}
def test_load_component_yaml_configs_extends_lists(tmp_path: Path) -> None:
"""Several directories can each add entries to one platform component."""
(tmp_path / "first").mkdir()
(tmp_path / "first" / "benchmark.yaml").write_text(
"uart:\n port: /dev/a\npacket_interface:\n - platform: uart\n"
)
(tmp_path / "second").mkdir()
(tmp_path / "second" / "benchmark.yaml").write_text(
"uart:\n port: /dev/b\npacket_interface:\n - platform: udp\n"
)
merged = build_helpers.load_component_yaml_configs(["first", "second"], tmp_path)
assert merged["packet_interface"] == [{"platform": "uart"}, {"platform": "udp"}]
# Other values are taken from the first directory that sets them
assert merged["uart"] == {"port": "/dev/a"}
+36
View File
@@ -0,0 +1,36 @@
"""Unit tests for the packet_interface component."""
import pytest
from esphome import config_validation as cv
from esphome.components import packet_interface
from esphome.components.packet_interface import CONF_MAX_PACKET_SIZE
from esphome.components.uart import packet_interface as uart_packet_interface
from esphome.components.udp import packet_interface as udp_packet_interface
@pytest.mark.parametrize(
("schema", "default", "limit"),
[
(uart_packet_interface.CONFIG_SCHEMA, 508, 4096),
(udp_packet_interface.CONFIG_SCHEMA, 508, 508),
],
)
def test_platform_max_packet_size(
setup_core, schema: cv.Schema, default: int, limit: int
) -> None:
"""Each platform has its own default and upper limit for max_packet_size."""
assert schema({})[CONF_MAX_PACKET_SIZE] == default
assert schema({CONF_MAX_PACKET_SIZE: limit})[CONF_MAX_PACKET_SIZE] == limit
with pytest.raises(cv.Invalid):
schema({CONF_MAX_PACKET_SIZE: limit + 1})
with pytest.raises(cv.Invalid):
schema({CONF_MAX_PACKET_SIZE: 15})
def test_schema_default(setup_core) -> None:
"""packet_interface_schema applies the default it is given."""
schema = packet_interface.packet_interface_schema(
packet_interface.PacketInterface, 100
)
assert schema({})[CONF_MAX_PACKET_SIZE] == 100