blob: b165986acd88894be5d28e57c19b007b9088def3 [file]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include <cstddef>
#include <limits>
#include "pw_allocator/allocator.h"
#include "pw_allocator/testing.h"
#include "pw_assert/check.h"
#include "pw_async2/basic_dispatcher.h"
#include "pw_async2/func_task.h"
#include "pw_async2/poll.h"
#include "pw_async2/task.h"
#include "pw_async2/try.h"
#include "pw_async2/waker.h"
#include "pw_bytes/endian.h"
#include "pw_bytes/span.h"
#include "pw_checksum/crc32.h"
#include "pw_multibuf/examples/protocol.h"
#include "pw_multibuf/multibuf.h"
#include "pw_status/try.h"
#include "pw_unit_test/framework.h"
namespace examples {
using pw::multibuf::examples::DemoLinkFooter;
using pw::multibuf::examples::DemoLinkHeader;
using pw::multibuf::examples::DemoNetworkHeader;
constexpr size_t kDemoLinkHeaderLen =
pw::multibuf::examples::kDemoLinkHeaderLen;
constexpr size_t kDemoLinkFooterLen =
pw::multibuf::examples::kDemoLinkFooterLen;
constexpr size_t kDemoNetworkHeaderLen =
pw::multibuf::examples::kDemoNetworkHeaderLen;
constexpr uint16_t kLinkSrcAddr = 0x3b15;
constexpr uint16_t kLinkDstAddr = 0x91a0;
constexpr uint64_t kNetSrcAddr = 0xdeadbeefcafef00d;
constexpr uint64_t kNetDstAddr = 0x123456789abcdef0;
// DOCSTAG: [pw_multibuf-examples-transfer-fields]
template <typename T>
constexpr T GetField(pw::ConstByteSpan data, size_t offset) {
return pw::bytes::ReadInOrder<T>(pw::endian::little, &data[offset]);
}
template <typename T>
constexpr void SetField(pw::ByteSpan data, size_t offset, T value) {
return pw::bytes::CopyInOrder<T>(pw::endian::little, value, &data[offset]);
}
// DOCSTAG: [pw_multibuf-examples-transfer-fields]
// DOCSTAG: [pw_multibuf-examples-transfer-network_packet]
class NetworkPacket {
public:
/// Create and a return a new network packet, or return an error if unable to
/// allocate the needed memory.
static pw::Result<NetworkPacket> Create(pw::Allocator& allocator) {
auto metadata = allocator.MakeUnique<std::byte[]>(kDemoNetworkHeaderLen);
if (metadata == nullptr) {
return pw::Status::ResourceExhausted();
}
NetworkPacket packet(allocator);
if (!packet.mbuf_->TryReserveForPushBack()) {
return pw::Status::ResourceExhausted();
}
packet.mbuf_->PushBack(std::move(metadata));
return packet;
}
void set_src_addr(uint64_t addr) {
SetField<uint64_t>(header(), offsetof(DemoNetworkHeader, src_addr), addr);
}
void set_dst_addr(uint64_t addr) {
SetField<uint64_t>(header(), offsetof(DemoNetworkHeader, dst_addr), addr);
}
/// Interpret the first chunk as a network packet header.
constexpr DemoNetworkHeader GetHeader() const {
return DemoNetworkHeader{
.src_addr =
GetField<uint64_t>(header(), offsetof(DemoNetworkHeader, src_addr)),
.dst_addr =
GetField<uint64_t>(header(), offsetof(DemoNetworkHeader, dst_addr)),
.length =
GetField<uint32_t>(header(), offsetof(DemoNetworkHeader, length)),
};
}
/// Add a payload to a network packet.
[[nodiscard]] bool AddPayload(pw::UniquePtr<std::byte[]>&& payload) {
if (!mbuf_->TryReserveForPushBack()) {
return false;
}
mbuf_->PushBack(std::move(payload));
size_t length = mbuf_->size();
PW_CHECK_UINT_LE(length, std::numeric_limits<uint32_t>::max());
SetField<uint32_t>(header(),
offsetof(DemoNetworkHeader, length),
static_cast<uint32_t>(length));
return true;
}
/// Consume a network packet and return its payload.
static pw::Result<pw::UniquePtr<std::byte[]>> ExtractPayload(
NetworkPacket&& packet) {
DemoNetworkHeader header = packet.GetHeader();
if (header.length != packet.mbuf_->size()) {
return pw::Status::DataLoss();
}
PW_TRY_ASSIGN(
auto iter,
packet.mbuf_->Discard(packet.mbuf_->cbegin(), kDemoNetworkHeaderLen));
return packet.mbuf_->Release(iter);
}
private:
constexpr NetworkPacket(pw::Allocator& allocator) : mbuf_(allocator) {}
friend class LinkFrame;
explicit NetworkPacket(pw::FlatMultiBuf&& mbuf) : mbuf_(std::move(mbuf)) {}
pw::ByteSpan header() { return *(mbuf_->Chunks().begin()); }
constexpr pw::ConstByteSpan header() const {
return *(mbuf_->ConstChunks().cbegin());
}
pw::FlatMultiBuf::Instance mbuf_;
};
// DOCSTAG: [pw_multibuf-examples-transfer-network_packet]
// DOCSTAG: [pw_multibuf-examples-transfer-link_frame]
class LinkFrame {
public:
/// Create and a return a new link frame, or return an error if unable to
/// allocate the needed memory.
static pw::Result<LinkFrame> Create(pw::Allocator& allocator) {
auto metadata = allocator.MakeUnique<std::byte[]>(kDemoLinkHeaderLen +
kDemoLinkFooterLen);
if (metadata == nullptr) {
return pw::Status::ResourceExhausted();
}
LinkFrame frame(allocator);
frame.mbuf_->PushBack(std::move(metadata));
return frame;
}
constexpr auto Chunks() { return mbuf_->Chunks(); }
constexpr auto ConstChunks() const { return mbuf_->ConstChunks(); }
void set_src_addr(uint16_t addr) {
SetField<uint16_t>(header(), offsetof(DemoLinkHeader, src_addr), addr);
}
void set_dst_addr(uint16_t addr) {
SetField<uint16_t>(header(), offsetof(DemoLinkHeader, dst_addr), addr);
}
/// Interpret the first chunk as a link frame header.
constexpr DemoLinkHeader GetHeader() const {
return DemoLinkHeader{
.src_addr =
GetField<uint16_t>(header(), offsetof(DemoLinkHeader, src_addr)),
.dst_addr =
GetField<uint16_t>(header(), offsetof(DemoLinkHeader, dst_addr)),
.length =
GetField<uint16_t>(header(), offsetof(DemoLinkHeader, length)),
};
}
/// Interpret the last chunk as a link frame footer.
constexpr DemoLinkFooter GetFooter() const {
return DemoLinkFooter{
.crc32 = GetField<uint32_t>(footer(), offsetof(DemoLinkFooter, crc32)),
};
}
/// Moves the given netrowk packet into the payload of this frame.
[[nodiscard]] bool AddNetworkPacket(NetworkPacket&& packet) {
auto iter = mbuf_->cend() - kDemoLinkFooterLen;
if (!mbuf_->TryReserveForInsert(iter, *packet.mbuf_)) {
return false;
}
mbuf_->Insert(iter, std::move(*packet.mbuf_));
size_t length = mbuf_->size();
PW_CHECK_UINT_LE(length, std::numeric_limits<uint16_t>::max());
SetField<uint16_t>(header(),
offsetof(DemoLinkHeader, length),
static_cast<uint16_t>(length));
return true;
}
/// Updates the checksum for the finished frame.
void Finalize() {
SetField<uint32_t>(
footer(), offsetof(DemoLinkFooter, crc32), CalculateCheckSum());
}
/// Examines a link frame. If it is valid, returns its payload as a network
/// packet, otherwise returns an error.
static pw::Result<NetworkPacket> ExtractNetworkPacket(LinkFrame&& frame) {
DemoLinkHeader header = frame.GetHeader();
DemoLinkFooter footer = frame.GetFooter();
if (header.length != frame.mbuf_->size() ||
footer.crc32 != frame.CalculateCheckSum()) {
return pw::Status::DataLoss();
}
uint32_t packet_length =
header.length - (kDemoLinkHeaderLen + kDemoLinkFooterLen);
auto iter = frame.mbuf_->cbegin();
PW_TRY_ASSIGN(iter, frame.mbuf_->Discard(iter, kDemoLinkHeaderLen));
iter += packet_length;
PW_TRY_ASSIGN(iter, frame.mbuf_->Discard(iter, kDemoLinkFooterLen));
return NetworkPacket(std::move(*frame.mbuf_));
}
private:
constexpr LinkFrame(pw::Allocator& allocator) : mbuf_(allocator) {}
uint32_t CalculateCheckSum() const {
pw::checksum::Crc32 crc32;
pw::ConstByteSpan prev;
for (pw::ConstByteSpan chunk : mbuf_->ConstChunks()) {
crc32.Update(prev);
prev = chunk;
}
return crc32.value();
}
constexpr pw::ByteSpan header() { return *(mbuf_->Chunks().begin()); }
constexpr pw::ConstByteSpan header() const {
return *(mbuf_->ConstChunks().cbegin());
}
constexpr pw::ByteSpan footer() { return *(--(mbuf_->Chunks().end())); }
constexpr pw::ConstByteSpan footer() const {
return *(--(mbuf_->ConstChunks().cend()));
}
pw::FlatMultiBuf::Instance mbuf_;
};
// DOCSTAG: [pw_multibuf-examples-transfer-link_frame]
/// Asynchronously forward that has be written to callers trying to read.
class Link {
public:
pw::async2::Poll<> Write(pw::async2::Context& context,
pw::ConstByteSpan tx_buffer) {
if (!pending_.has_value()) {
pending_ = tx_buffer;
rx_waker_.Wake();
} else if (pending_->empty()) {
pending_.reset();
return pw::async2::Ready();
}
PW_ASYNC_STORE_WAKER(context, tx_waker_, "transmitting data");
return pw::async2::Pending();
}
pw::async2::Poll<size_t> Read(pw::async2::Context& context,
pw::ByteSpan rx_buffer) {
if (!pending_.has_value() || pending_->empty()) {
PW_ASYNC_STORE_WAKER(context, rx_waker_, "waiting for data");
return pw::async2::Pending();
}
size_t len = std::min(pending_->size(), rx_buffer.size());
std::memcpy(rx_buffer.data(), pending_->data(), len);
pending_ = pending_->subspan(len);
tx_waker_.Wake();
return pw::async2::Ready(len);
}
private:
std::optional<pw::ConstByteSpan> pending_;
pw::async2::Waker tx_waker_;
pw::async2::Waker rx_waker_;
};
const char* kLoremIpsum =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum.";
TEST(LinkTest, SendAndReceiveData) {
pw::allocator::test::AllocatorForTest<2048> allocator;
Link link;
pw::async2::BasicDispatcher dispatcher;
auto tx_payload =
allocator.MakeUnique<std::byte[]>(std::strlen(kLoremIpsum) + 1);
ASSERT_NE(tx_payload, nullptr);
std::memcpy(tx_payload.get(), kLoremIpsum, tx_payload.size());
// DOCSTAG: [pw_multibuf-examples-transfer-create]
pw::Result<NetworkPacket> tx_packet = NetworkPacket::Create(allocator);
ASSERT_EQ(tx_packet.status(), pw::OkStatus());
tx_packet->set_src_addr(kNetSrcAddr);
tx_packet->set_dst_addr(kNetDstAddr);
ASSERT_TRUE(tx_packet->AddPayload(std::move(tx_payload)));
pw::Result<LinkFrame> tx_frame = LinkFrame::Create(allocator);
ASSERT_EQ(tx_frame.status(), pw::OkStatus());
tx_frame->set_src_addr(kLinkSrcAddr);
tx_frame->set_dst_addr(kLinkDstAddr);
ASSERT_TRUE(tx_frame->AddNetworkPacket(std::move(*tx_packet)));
tx_frame->Finalize();
// DOCSTAG: [pw_multibuf-examples-transfer-create]
auto tx_iter = tx_frame->ConstChunks().begin();
pw::async2::FuncTask write_frame(
[&](pw::async2::Context& context) -> pw::async2::Poll<> {
while (tx_iter != tx_frame->ConstChunks().end()) {
if (!tx_iter->empty()) {
PW_TRY_READY(link.Write(context, *tx_iter));
}
++tx_iter;
}
return pw::async2::Ready();
});
dispatcher.Post(write_frame);
pw::Result<LinkFrame> rx_frame = LinkFrame::Create(allocator);
ASSERT_EQ(rx_frame.status(), pw::OkStatus());
pw::ByteSpan raw_frame_header =
rx_frame->Chunks().begin()->subspan(0, kDemoLinkHeaderLen);
pw::Result<NetworkPacket> rx_packet = NetworkPacket::Create(allocator);
ASSERT_EQ(rx_packet.status(), pw::OkStatus());
bool read_frame_header_finished = false;
pw::async2::FuncTask read_frame_header(
[&](pw::async2::Context& context) -> pw::async2::Poll<> {
while (!raw_frame_header.empty()) {
PW_TRY_READY_ASSIGN(size_t bytes_read,
link.Read(context, raw_frame_header));
raw_frame_header = raw_frame_header.subspan(bytes_read);
}
read_frame_header_finished = true;
return pw::async2::Ready();
});
dispatcher.Post(read_frame_header);
dispatcher.RunUntilStalled();
EXPECT_TRUE(read_frame_header_finished);
DemoLinkHeader frame_header = rx_frame->GetHeader();
EXPECT_EQ(frame_header.src_addr, kLinkSrcAddr);
EXPECT_EQ(frame_header.dst_addr, kLinkDstAddr);
size_t payload_len =
frame_header.length -
(kDemoLinkHeaderLen + kDemoNetworkHeaderLen + kDemoLinkFooterLen);
auto rx_payload = allocator.MakeUnique<std::byte[]>(payload_len);
ASSERT_NE(rx_payload, nullptr);
ASSERT_TRUE(rx_packet->AddPayload(std::move(rx_payload)));
ASSERT_TRUE(rx_frame->AddNetworkPacket(std::move(*rx_packet)));
auto iter = rx_frame->Chunks().begin();
pw::ByteSpan chunk = *(++iter);
pw::async2::FuncTask read_remaining_frame(
[&](pw::async2::Context& context) -> pw::async2::Poll<> {
while (true) {
PW_TRY_READY_ASSIGN(size_t bytes_read, link.Read(context, chunk));
chunk = chunk.subspan(bytes_read);
if (!chunk.empty()) {
continue;
}
++iter;
if (iter == rx_frame->ConstChunks().end()) {
break;
}
chunk = *iter;
}
return pw::async2::Ready();
});
dispatcher.Post(read_remaining_frame);
dispatcher.RunToCompletion();
rx_packet = LinkFrame::ExtractNetworkPacket(std::move(*rx_frame));
ASSERT_EQ(rx_packet.status(), pw::OkStatus());
DemoNetworkHeader packet_header = rx_packet->GetHeader();
EXPECT_EQ(packet_header.src_addr, kNetSrcAddr);
EXPECT_EQ(packet_header.dst_addr, kNetDstAddr);
auto rx_payload_result = NetworkPacket::ExtractPayload(std::move(*rx_packet));
ASSERT_EQ(rx_payload_result.status(), pw::OkStatus());
rx_payload = std::move(rx_payload_result.value());
auto* rx_text = reinterpret_cast<const char*>(rx_payload.get());
EXPECT_STREQ(rx_text, kLoremIpsum);
}
} // namespace examples