mirror of
https://github.com/esphome/esphome.git
synced 2026-10-21 21:36:14 +00:00
Layers stack on a link and are configured on its packet_interface entry. Each layer type registers itself with its kind, so a new type needs only a new module. The first is a reliability layer in the style of HDLC asynchronous balanced mode, with go-back-N retransmission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
309 lines
9.1 KiB
C++
309 lines
9.1 KiB
C++
#include <gtest/gtest.h>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <vector>
|
|
#include "esphome/components/packet_interface/hdlc_reliability.h"
|
|
#include "esphome/core/application.h"
|
|
#include "esphome/core/helpers.h"
|
|
|
|
namespace esphome::packet_interface::testing {
|
|
|
|
using Frame = std::vector<uint8_t>;
|
|
|
|
// A link layer that records what is sent, so a test can deliver it to the peer
|
|
class MockLink : public PacketInterface {
|
|
public:
|
|
MockLink() { this->set_max_packet_size(64); }
|
|
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
|
|
if (this->budget == 0) {
|
|
this->refused++;
|
|
return false;
|
|
}
|
|
this->budget--;
|
|
this->sent.push_back(data.to_vector());
|
|
return true;
|
|
}
|
|
void inject(const Frame &frame) { this->on_receive_from_interface_(PacketBuffer(frame), {}); }
|
|
|
|
std::vector<Frame> sent;
|
|
size_t budget{SIZE_MAX}; // frames accepted before the link refuses, like a full network stack
|
|
size_t refused{0};
|
|
};
|
|
|
|
class TestHdlc : public HdlcReliability {
|
|
public:
|
|
using HdlcReliability::HdlcReliability;
|
|
using HdlcReliability::on_timeout_;
|
|
using HdlcReliability::outstanding_;
|
|
};
|
|
|
|
struct Peer {
|
|
explicit Peer(uint8_t queue_length = 4) {
|
|
this->hdlc = std::make_unique<TestHdlc>(&this->link);
|
|
this->hdlc->set_max_queue_length(queue_length);
|
|
this->hdlc->set_max_retries(3);
|
|
this->hdlc->setup();
|
|
this->hdlc->add_packet_interface_listener(
|
|
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(data.to_vector()); });
|
|
this->hdlc->add_on_session_state_callback([this](bool connected) { this->states.push_back(connected); });
|
|
}
|
|
bool send(const Frame &data) { return this->hdlc->send_to_interface(PacketBuffer(data), {}); }
|
|
|
|
MockLink link;
|
|
std::unique_ptr<TestHdlc> hdlc;
|
|
std::vector<Frame> received;
|
|
std::vector<bool> states;
|
|
};
|
|
|
|
// Returns true to drop a frame on its way from one peer to the other
|
|
using DropFilter = std::function<bool(const Frame &)>;
|
|
|
|
static bool is_info(const Frame &frame) { return (frame[0] & 1) == 0; }
|
|
static bool is_ack(const Frame &frame) { return (frame[0] & 0x0F) == 0x01; }
|
|
|
|
// Move frames between the peers and run their loops until nothing more is sent
|
|
static void pump(Peer &a, Peer &b, const DropFilter &drop_a_to_b = nullptr, const DropFilter &drop_b_to_a = nullptr) {
|
|
for (int i = 0; i != 50; i++) {
|
|
a.hdlc->loop();
|
|
b.hdlc->loop();
|
|
if (a.link.sent.empty() && b.link.sent.empty())
|
|
return;
|
|
auto from_a = std::move(a.link.sent);
|
|
auto from_b = std::move(b.link.sent);
|
|
a.link.sent.clear();
|
|
b.link.sent.clear();
|
|
for (auto &frame : from_a) {
|
|
if (!drop_a_to_b || !drop_a_to_b(frame))
|
|
b.link.inject(frame);
|
|
}
|
|
for (auto &frame : from_b) {
|
|
if (!drop_b_to_a || !drop_b_to_a(frame))
|
|
a.link.inject(frame);
|
|
}
|
|
}
|
|
FAIL() << "Peers did not go quiet";
|
|
}
|
|
|
|
static Frame make_frame(uint8_t control, const Frame &payload) {
|
|
Frame frame{control};
|
|
frame.insert(frame.end(), payload.begin(), payload.end());
|
|
uint16_t crc = crc16(frame.data(), frame.size());
|
|
frame.push_back(crc & 0xFF);
|
|
frame.push_back(crc >> 8);
|
|
return frame;
|
|
}
|
|
|
|
// The test main does not construct App; the retransmit timer needs its scheduler
|
|
class HdlcReliabilityTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
new (&App) Application;
|
|
App.pre_setup("hdlc-tests", 10, "", 0);
|
|
}
|
|
void TearDown() override { App.~Application(); }
|
|
};
|
|
|
|
TEST_F(HdlcReliabilityTest, MaxPacketSizeAllowsForOverhead) {
|
|
Peer a;
|
|
EXPECT_EQ(a.hdlc->get_max_packet_size(), 64 - HdlcReliability::FRAME_OVERHEAD);
|
|
EXPECT_EQ(a.link.top(), a.hdlc.get());
|
|
EXPECT_EQ(a.hdlc->get_capabilities(), CAPABILITY_RELIABILITY);
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, ResetHandshake) {
|
|
Peer a, b;
|
|
a.hdlc->loop();
|
|
ASSERT_EQ(a.link.sent.size(), 1);
|
|
EXPECT_EQ(a.link.sent[0], make_frame(0x2F, {})); // SABM
|
|
pump(a, b);
|
|
EXPECT_TRUE(a.hdlc->is_connected());
|
|
EXPECT_TRUE(b.hdlc->is_connected());
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, QueuedBeforeConnectIsSentAfter) {
|
|
Peer a, b;
|
|
ASSERT_TRUE(a.send({0x01}));
|
|
ASSERT_TRUE(a.send({0x02}));
|
|
pump(a, b);
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0x01}, {0x02}}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, DeliversInOrderBothWays) {
|
|
Peer a, b;
|
|
pump(a, b);
|
|
a.send({0x10});
|
|
b.send({0x20});
|
|
a.send({0x11});
|
|
pump(a, b);
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0x10}, {0x11}}));
|
|
EXPECT_EQ(a.received, (std::vector<Frame>{{0x20}}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, SequenceNumbersWrap) {
|
|
Peer a(3), b(3);
|
|
pump(a, b);
|
|
std::vector<Frame> expected;
|
|
for (uint8_t i = 0; i != 20; i++) {
|
|
expected.push_back({i, static_cast<uint8_t>(i * 3)});
|
|
ASSERT_TRUE(a.send(expected.back()));
|
|
if (i % 2 == 1)
|
|
pump(a, b);
|
|
}
|
|
pump(a, b);
|
|
EXPECT_EQ(b.received, expected);
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, GapRecoveredByReject) {
|
|
Peer a, b;
|
|
pump(a, b);
|
|
bool dropped = false;
|
|
a.send({0x01});
|
|
a.send({0x02});
|
|
a.send({0x03});
|
|
pump(a, b, [&dropped](const Frame &frame) {
|
|
if (dropped || !is_info(frame))
|
|
return false;
|
|
dropped = true;
|
|
return true;
|
|
});
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0x01}, {0x02}, {0x03}}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, LossRecoveredByTimeout) {
|
|
Peer a, b;
|
|
pump(a, b);
|
|
a.send({0x42});
|
|
pump(a, b, [](const Frame &frame) { return is_info(frame); });
|
|
EXPECT_TRUE(b.received.empty());
|
|
a.hdlc->on_timeout_();
|
|
pump(a, b);
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0x42}}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, LostAckDoesNotDuplicate) {
|
|
Peer a, b;
|
|
pump(a, b);
|
|
a.send({0x42});
|
|
pump(a, b, nullptr, [](const Frame &frame) { return is_ack(frame); });
|
|
ASSERT_EQ(b.received.size(), 1);
|
|
a.hdlc->on_timeout_();
|
|
pump(a, b);
|
|
EXPECT_EQ(b.received.size(), 1);
|
|
// The resend was acknowledged, so the full queue is available again
|
|
for (int i = 0; i != 4; i++)
|
|
EXPECT_TRUE(a.send({0x01}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, QueueFull) {
|
|
Peer a(2), b(2);
|
|
pump(a, b);
|
|
EXPECT_TRUE(a.send({0x01}));
|
|
EXPECT_TRUE(a.send({0x02}));
|
|
EXPECT_FALSE(a.send({0x03}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, OversizedPacketRejected) {
|
|
Peer a;
|
|
EXPECT_FALSE(a.send(Frame(a.hdlc->get_max_packet_size() + 1)));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, CrcErrorDropped) {
|
|
Peer a, b;
|
|
pump(a, b);
|
|
Frame frame = make_frame(0x00, {0x55}); // I-frame N(S)=0 N(R)=0
|
|
frame[1] ^= 0xFF;
|
|
b.link.inject(frame);
|
|
EXPECT_TRUE(b.received.empty());
|
|
b.link.inject(make_frame(0x00, {0x55}));
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0x55}}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, RetriesExhaustedResetsLink) {
|
|
Peer a, b;
|
|
pump(a, b);
|
|
a.send({0x01});
|
|
pump(a, b, [](const Frame &frame) { return is_info(frame); });
|
|
for (int i = 0; i != 4; i++) {
|
|
a.hdlc->on_timeout_();
|
|
pump(a, b, [](const Frame &frame) { return is_info(frame); });
|
|
}
|
|
// The unacknowledged frame was discarded and the link was reset, which both report as data lost
|
|
EXPECT_TRUE(b.received.empty());
|
|
EXPECT_TRUE(a.hdlc->is_connected());
|
|
EXPECT_EQ(a.states, std::vector<bool>{false});
|
|
EXPECT_EQ(b.states, std::vector<bool>{false});
|
|
a.send({0x02});
|
|
pump(a, b);
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0x02}}));
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, PeerRestart) {
|
|
Peer a;
|
|
auto b = std::make_unique<Peer>();
|
|
pump(a, *b);
|
|
a.send({0x01});
|
|
pump(a, *b);
|
|
// The peer restarts with fresh sequence numbers
|
|
b = std::make_unique<Peer>();
|
|
pump(a, *b);
|
|
a.send({0x02});
|
|
b->send({0x03});
|
|
pump(a, *b);
|
|
EXPECT_EQ(b->received, (std::vector<Frame>{{0x02}}));
|
|
EXPECT_EQ(a.received, (std::vector<Frame>{{0x03}}));
|
|
// The restarted peer's reset discarded what a had not yet had acknowledged
|
|
EXPECT_EQ(a.states, std::vector<bool>{false});
|
|
EXPECT_TRUE(b->states.empty());
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, RefusedFramesSentAgainWithoutTimeout) {
|
|
Peer a, b;
|
|
for (uint8_t i = 0; i != 3; i++)
|
|
ASSERT_TRUE(a.send({i}));
|
|
a.link.budget = 3; // SABM, UA to the peer's SABM, and the first queued frame
|
|
pump(a, b);
|
|
EXPECT_GE(a.link.refused, 1);
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0}}));
|
|
a.link.budget = SIZE_MAX; // the lower layer accepts again, as when a radio stops receiving
|
|
pump(a, b);
|
|
EXPECT_EQ(b.received, (std::vector<Frame>{{0}, {1}, {2}}));
|
|
EXPECT_EQ(a.hdlc->outstanding_(), 0);
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, RefusedAckSentAgain) {
|
|
Peer a, b;
|
|
pump(a, b);
|
|
a.send({1});
|
|
for (auto &frame : a.link.sent)
|
|
b.link.inject(frame);
|
|
a.link.sent.clear();
|
|
b.link.budget = 0;
|
|
b.hdlc->loop();
|
|
EXPECT_TRUE(b.link.sent.empty());
|
|
b.link.budget = SIZE_MAX;
|
|
pump(a, b);
|
|
EXPECT_EQ(a.hdlc->outstanding_(), 0);
|
|
}
|
|
|
|
TEST_F(HdlcReliabilityTest, AckDelayLetsReplyCarryAck) {
|
|
Peer a, b;
|
|
b.hdlc->set_ack_delay(100);
|
|
pump(a, b);
|
|
a.send({1});
|
|
for (auto &frame : a.link.sent)
|
|
b.link.inject(frame);
|
|
a.link.sent.clear();
|
|
b.hdlc->loop();
|
|
EXPECT_TRUE(b.link.sent.empty()); // no RR yet
|
|
b.send({2});
|
|
ASSERT_EQ(b.link.sent.size(), 1);
|
|
EXPECT_TRUE(is_info(b.link.sent[0]));
|
|
EXPECT_EQ(b.link.sent[0][0] >> 5, 1); // N(R) acknowledges the frame from a
|
|
b.link.sent.clear();
|
|
b.hdlc->loop();
|
|
EXPECT_TRUE(b.link.sent.empty()); // the RR is no longer owed
|
|
}
|
|
|
|
} // namespace esphome::packet_interface::testing
|