blob: 7a39d744f0a2282489d7e1dbb18ebf8850c34ef9 [file]
// Copyright 2026 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 <sys/types.h>
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <mutex>
#include <span>
#include "pw_allocator/libc_allocator.h"
#include "pw_allocator/testing.h"
#include "pw_assert/check.h"
#include "pw_bluetooth/emboss_util.h"
#include "pw_bluetooth/hci_commands.emb.h"
#include "pw_bluetooth/hci_common.emb.h"
#include "pw_bluetooth/hci_data.emb.h"
#include "pw_bluetooth/hci_events.emb.h"
#include "pw_bluetooth/hci_h4.emb.h"
#include "pw_bluetooth/l2cap_frames.emb.h"
#include "pw_bluetooth_proxy/direction.h"
#include "pw_bluetooth_proxy/h4_packet.h"
#include "pw_bluetooth_proxy/internal/logical_transport.h"
#include "pw_bluetooth_proxy/l2cap_channel_common.h"
#include "pw_bluetooth_proxy/l2cap_status_delegate.h"
#include "pw_bluetooth_proxy/proxy_host.h"
#include "pw_bluetooth_proxy_private/test_utils.h"
#include "pw_containers/flat_map.h"
#include "pw_containers/vector.h"
#include "pw_function/function.h"
#include "pw_log/log.h"
#include "pw_multibuf/from_span.h"
#include "pw_multibuf/multibuf.h"
#include "pw_span/cast.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_sync/binary_semaphore.h"
#include "pw_sync/mutex.h"
#include "pw_thread/sleep.h"
#include "pw_thread/test_thread_context.h"
#include "pw_thread/thread.h"
#include "pw_unit_test/framework.h"
#include "pw_unit_test/status_macros.h"
namespace pw::bluetooth::proxy {
namespace {
class DynamicCreditSharingTest : public ProxyHostTest {
public:
using ProxyHostTest::SendNumberOfCompletedPackets;
};
TEST_F(DynamicCreditSharingTest, DynamicQueueingAndDrainingLE) {
struct {
int sends_called = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&&) { capture.sends_called++; });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
// Set total credits to 2.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2));
constexpr uint16_t connection_handle = 0x123;
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, connection_handle, emboss::StatusCode::SUCCESS));
GattNotifyChannel channel =
BuildGattNotifyChannel(proxy, {.handle = connection_handle});
std::array<uint8_t, 1> attribute_value = {7};
// Send first packet.
{
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 1);
}
// Send second packet.
{
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 2);
}
// Send third packet (should be queued).
{
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
// Verify it was NOT sent to controller yet.
EXPECT_EQ(capture.sends_called, 2);
}
// Reclaim 1 credit.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
// The queued packet should be sent out.
EXPECT_EQ(capture.sends_called, 3);
}
TEST_F(DynamicCreditSharingTest, DisconnectClearsQueuedPackets) {
struct {
int sends_called = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&&) { capture.sends_called++; });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
// Set total credits to 1.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
constexpr uint16_t connection_handle = 0x123;
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, connection_handle, emboss::StatusCode::SUCCESS));
GattNotifyChannel channel =
BuildGattNotifyChannel(proxy, {.handle = connection_handle});
std::array<uint8_t, 1> attribute_value = {7};
// Send first packet (uses 1 credit).
{
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 1);
}
// Send second packet (should be queued in AclDataChannel).
{
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 1); // Still 1
}
// Send Disconnection_Complete event.
PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, connection_handle));
// Reclaim 1 credit.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
// The queued packet should NOT be sent out because it was cleared.
EXPECT_EQ(capture.sends_called, 1);
}
TEST_F(DynamicCreditSharingTest, DynamicQueueingAndDrainingBrEdr) {
struct {
int sends_called = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&&) { capture.sends_called++; });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
// Set total credits to 2 for BR/EDR.
PW_TEST_EXPECT_OK(SendReadBufferResponseFromController(proxy, 2));
constexpr uint16_t connection_handle = 0x456;
PW_TEST_ASSERT_OK(SendConnectionCompleteEvent(
proxy, connection_handle, emboss::StatusCode::SUCCESS));
BasicL2capChannel channel = BuildBasicL2capChannel(
proxy,
{.handle = connection_handle, .transport = AclTransportType::kBrEdr});
std::array<uint8_t, 1> attribute_value = {7};
// Send first packet.
{
auto mbuf_result =
multibuf::FromSpan(*GetProxyHostAllocator(),
as_writable_bytes(span(attribute_value)),
[](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
PW_TEST_EXPECT_OK(channel.Write(std::move(*mbuf_result)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 1);
}
// Send second packet.
{
auto mbuf_result =
multibuf::FromSpan(*GetProxyHostAllocator(),
as_writable_bytes(span(attribute_value)),
[](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
PW_TEST_EXPECT_OK(channel.Write(std::move(*mbuf_result)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 2);
}
// Send third packet (should be queued).
{
auto mbuf_result =
multibuf::FromSpan(*GetProxyHostAllocator(),
as_writable_bytes(span(attribute_value)),
[](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
PW_TEST_EXPECT_OK(channel.Write(std::move(*mbuf_result)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 2);
}
// Reclaim 1 credit.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
// The queued packet should be sent out.
EXPECT_EQ(capture.sends_called, 3);
}
TEST_F(DynamicCreditSharingTest, HostCreditsExhaustedThenProxyQueuesLE) {
struct {
int sends_called = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&&) { capture.sends_called++; });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
// Set total credits to 2.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2));
constexpr uint16_t connection_handle = 0x123;
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, connection_handle, emboss::StatusCode::SUCCESS));
GattNotifyChannel channel =
BuildGattNotifyChannel(proxy, {.handle = connection_handle});
// Simulate Host sending 2 packets.
{
std::array<uint8_t, 9> host_packet_arr = {
0x02,
0x23,
0x01,
0x04,
0x00, // H4 Type, Connection Handle 0x123, Length = 4
0x00,
0x00,
0x01,
0x00 // L2CAP Header: Length = 0, CID = 1
};
proxy.HandleH4HciFromHost(H4PacketWithH4(host_packet_arr));
proxy.HandleH4HciFromHost(H4PacketWithH4(host_packet_arr));
}
EXPECT_EQ(capture.sends_called, 2);
std::array<uint8_t, 1> attribute_value = {7};
// Send packet from proxy (should be queued).
{
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 2);
}
// Reclaim 1 credit.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
// The queued packet should be sent out.
EXPECT_EQ(capture.sends_called, 3);
}
TEST_F(DynamicCreditSharingTest, HostCreditsExhaustedThenProxyQueuesBrEdr) {
struct {
int sends_called = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&&) { capture.sends_called++; });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
// Set total credits to 2 for BR/EDR.
PW_TEST_EXPECT_OK(SendReadBufferResponseFromController(proxy, 2));
constexpr uint16_t connection_handle = 0x456;
PW_TEST_ASSERT_OK(SendConnectionCompleteEvent(
proxy, connection_handle, emboss::StatusCode::SUCCESS));
BasicL2capChannel channel = BuildBasicL2capChannel(
proxy,
{.handle = connection_handle, .transport = AclTransportType::kBrEdr});
// Simulate Host sending 2 packets.
{
std::array<uint8_t, 9> host_packet_arr = {
0x02,
0x56,
0x04,
0x04,
0x00, // H4 Type, Connection Handle 0x456, Length = 4
0x00,
0x00,
0x01,
0x00 // L2CAP Header: Length = 0, CID = 1
};
proxy.HandleH4HciFromHost(H4PacketWithH4(host_packet_arr));
proxy.HandleH4HciFromHost(H4PacketWithH4(host_packet_arr));
}
EXPECT_EQ(capture.sends_called, 2);
std::array<uint8_t, 1> attribute_value = {7};
// Send packet from proxy (should be queued).
{
auto mbuf_result =
multibuf::FromSpan(*GetProxyHostAllocator(),
as_writable_bytes(span(attribute_value)),
[](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
PW_TEST_EXPECT_OK(channel.Write(std::move(*mbuf_result)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 2);
}
// Reclaim 1 credit.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
// The queued packet should be sent out.
EXPECT_EQ(capture.sends_called, 3);
}
TEST_F(DynamicCreditSharingTest, FairRoundRobinDrainingAndNocp) {
struct {
int sends_to_controller = 0;
int proxy_packets_sent = 0;
int host_packets_sent = 0;
int nocps_to_host = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[&capture](H4PacketWithHci&& packet) {
pw::span<uint8_t> hci_buffer = packet.GetHciSpan();
Result<emboss::EventHeaderView> event =
MakeEmbossView<emboss::EventHeaderView>(hci_buffer);
if (event.ok() && event->event_code().Read() ==
emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) {
Result<emboss::NumberOfCompletedPacketsEventView> nocp =
MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>(
hci_buffer);
if (nocp.ok() && nocp->num_handles().Read() > 0) {
capture.nocps_to_host +=
nocp->nocp_data()[0].num_completed_packets().Read();
}
}
});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
capture.sends_to_controller++;
Result<emboss::AclDataFrameHeaderView> acl_view =
MakeEmbossView<emboss::AclDataFrameHeaderView>(packet.GetHciSpan());
if (acl_view.ok()) {
// Proxy GattNotifyChannel packets in this test have data length 8.
// Host packets have length 6.
if (acl_view->data_total_length().Read() == 8) {
capture.proxy_packets_sent++;
} else {
PW_LOG_INFO("ACL length: %d", acl_view->data_total_length().Read());
capture.host_packets_sent++;
}
}
});
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2));
constexpr uint16_t connection_handle = 0x123;
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, connection_handle, emboss::StatusCode::SUCCESS));
GattNotifyChannel channel =
BuildGattNotifyChannel(proxy, {.handle = connection_handle});
// Host sends 4 packets (length = 4 + 2 = 6, data length = 2)
std::array<std::array<uint8_t, 11>, 4> host_packets;
for (size_t i = 0; i < 4; ++i) {
host_packets[i] = {
0x02,
0x23,
0x01,
0x06,
0x00, // H4, Handle 0x123, Length = 6
0x02,
0x00,
0x01,
0x00, // L2CAP: Length = 2, CID = 1
0xAA,
0xBB // Payload
};
proxy.HandleH4HciFromHost(H4PacketWithH4(host_packets[i]));
}
// Proxy sends 4 packets (length = 4 + 1 = 5, data length = 1)
std::array<uint8_t, 1> attribute_value = {7};
for (int i = 0; i < 4; ++i) {
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
}
RunDispatcher();
// Initially, controller max is 2. 2 host packets sent, 2 queued. 4 proxy
// queued.
EXPECT_EQ(capture.sends_to_controller, 2);
EXPECT_EQ(capture.host_packets_sent, 2);
EXPECT_EQ(capture.proxy_packets_sent, 0);
EXPECT_EQ(capture.nocps_to_host, 0);
// Reclaim 1
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
RunDispatcher();
// We want to see round-robin. Either proxy or host sends.
EXPECT_EQ(capture.sends_to_controller, 3);
// Reclaim 3 more
for (int i = 0; i < 3; ++i) {
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
RunDispatcher();
}
// After 4 reclaims, we should have sent a mix of host and proxy packets.
// Total packets sent = 2 + 4 = 6.
EXPECT_EQ(capture.sends_to_controller, 6);
EXPECT_TRUE(capture.proxy_packets_sent > 0);
EXPECT_TRUE(capture.host_packets_sent > 2); // At least one from queue
}
#if PW_THREAD_JOINING_ENABLED
TEST_F(DynamicCreditSharingTest, StressMultiThreadedSend) {
constexpr unsigned int kNumThreads = 4;
constexpr unsigned int kPacketsPerThread = 20;
constexpr uint16_t kTestHandle = 0x123;
constexpr uint16_t kPayloadSize = 10;
constexpr uint16_t kTotalCredits = 5;
constexpr uint16_t kBaseLocalCid = 0xb000;
constexpr uint16_t kBaseRemoteCid = 0xc000;
struct {
std::atomic<int> sends_to_controller{0};
std::atomic<int> proxy_packets_sent{0};
std::atomic<int> host_packets_sent{0};
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
capture.sends_to_controller++;
Result<emboss::AclDataFrameHeaderView> acl_view =
MakeEmbossView<emboss::AclDataFrameHeaderView>(packet.GetHciSpan());
if (acl_view.ok()) {
if (acl_view->data_total_length().Read() == kPayloadSize + 4) {
capture.proxy_packets_sent++;
} else {
capture.host_packets_sent++;
}
}
});
// Use libc allocators so msan can detect use after frees.
std::array<std::byte, 20 * 1024> packet_buffer{};
pw::multibuf::SimpleAllocator multibuf_allocator{
/*data_area=*/packet_buffer,
/*metadata_alloc=*/allocator::GetLibCAllocator()};
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnNewThread(proxy);
PW_TEST_EXPECT_OK(
SendLeReadBufferResponseFromController(proxy, kTotalCredits));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kTestHandle, emboss::StatusCode::SUCCESS));
struct ThreadCapture {
BasicL2capChannel channel;
multibuf::MultiBufAllocator& packet_allocator;
};
pw::Vector<ThreadCapture, kNumThreads> captures;
for (unsigned int i = 0; i < kNumThreads; ++i) {
captures.emplace_back(ThreadCapture{
BuildBasicL2capChannel(
proxy,
BasicL2capParameters{
.handle = kTestHandle,
.local_cid = static_cast<uint16_t>(kBaseLocalCid + i),
.remote_cid = static_cast<uint16_t>(kBaseRemoteCid + i)}),
multibuf_allocator,
});
}
pw::thread::test::TestThreadContext context;
pw::Vector<pw::Thread, kNumThreads + 2> threads;
// Proxy writer threads
for (unsigned int i = 0; i < kNumThreads; ++i) {
ThreadCapture& thread_capture = captures[i];
threads.emplace_back(context.options(), [&thread_capture]() {
std::array<uint8_t, kPayloadSize> attribute_value{};
for (unsigned int j = 0; j < kPacketsPerThread; ++j) {
std::fill(attribute_value.begin(), attribute_value.end(), j);
auto mbuf_result =
thread_capture.packet_allocator.AllocateContiguous(kPayloadSize);
if (mbuf_result.has_value()) {
multibuf::MultiBuf mbuf_inst = std::move(*mbuf_result);
auto it = mbuf_inst.begin();
for (uint8_t val : attribute_value) {
*it = static_cast<std::byte>(val);
++it;
}
thread_capture.channel.Write(std::move(mbuf_inst));
}
pw::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
}
// Host writer thread
std::atomic<bool> stop_host{false};
struct HostContext {
ProxyHost& proxy;
std::atomic<bool>& stop;
} host_ctx{proxy, stop_host};
threads.emplace_back(context.options(), [&host_ctx]() {
std::array<uint8_t, 11> host_packet_arr = {
0x02,
0x23,
0x01,
0x06,
0x00, // H4, Handle 0x123, Length = 6
0x02,
0x00,
0x01,
0x00, // L2CAP: Length = 2, CID = 1
0xAA,
0xBB // Payload
};
while (!host_ctx.stop) {
host_ctx.proxy.HandleH4HciFromHost(H4PacketWithH4(host_packet_arr));
pw::this_thread::sleep_for(std::chrono::milliseconds(2));
}
});
// NOCP reclaimer thread
std::atomic<bool> stop_nocp{false};
struct NocpContext {
ProxyHost& proxy;
std::atomic<bool>& stop;
uint16_t handle;
DynamicCreditSharingTest* test;
} nocp_ctx{proxy, stop_nocp, kTestHandle, this};
threads.emplace_back(context.options(), [&nocp_ctx]() {
while (!nocp_ctx.stop) {
(void)nocp_ctx.test->SendNumberOfCompletedPackets(nocp_ctx.proxy,
{{nocp_ctx.handle, 1}});
pw::this_thread::sleep_for(std::chrono::milliseconds(3));
}
});
// Wait for proxy writers
for (unsigned int i = 0; i < kNumThreads; ++i) {
threads[i].join();
}
// Stop host and NOCP threads
stop_host = true;
stop_nocp = true;
threads[kNumThreads].join();
threads[kNumThreads + 1].join();
captures.clear();
JoinDispatcherThread();
EXPECT_GT(capture.sends_to_controller, 0);
EXPECT_GT(capture.proxy_packets_sent, 0);
EXPECT_GT(capture.host_packets_sent, 0);
}
#endif // PW_THREAD_JOINING_ENABLED
TEST_F(DynamicCreditSharingTest, HostPacketsOrderingStress) {
constexpr unsigned int kPacketsPerThread = 100;
constexpr uint16_t kTestHandle = 0x123;
constexpr uint16_t kInitialCredits = 5;
struct {
pw::Vector<uint8_t, kPacketsPerThread> sequence;
pw::sync::Mutex mutex;
int credits = kInitialCredits;
size_t nocp_sent = 0;
std::atomic<bool> stop_controller{false};
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[&capture](H4PacketWithHci&& packet) {
pw::span<uint8_t> hci_buffer = packet.GetHciSpan();
Result<emboss::EventHeaderView> event =
MakeEmbossView<emboss::EventHeaderView>(hci_buffer);
if (event.ok() && event->event_code().Read() ==
emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) {
Result<emboss::NumberOfCompletedPacketsEventView> nocp =
MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>(
hci_buffer);
if (nocp.ok() && nocp->num_handles().Read() > 0) {
std::lock_guard lock(capture.mutex);
capture.credits +=
nocp->nocp_data()[0].num_completed_packets().Read();
}
}
});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
pw::span<uint8_t> span = packet.GetHciSpan();
if (span.size() >= 10) {
uint8_t seq = span[9];
std::lock_guard lock(capture.mutex);
capture.sequence.push_back(seq);
}
});
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnNewThread(proxy);
PW_TEST_EXPECT_OK(
SendLeReadBufferResponseFromController(proxy, kInitialCredits));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kTestHandle, emboss::StatusCode::SUCCESS));
pw::thread::test::TestThreadContext context;
pw::Vector<pw::Thread, 2> threads;
// Host thread
struct HostThreadCtx {
ProxyHost& proxy;
decltype(capture)& cap;
} host_thread_ctx{proxy, capture};
threads.emplace_back(context.options(), [&host_thread_ctx]() {
for (unsigned int j = 0; j < kPacketsPerThread; ++j) {
bool has_credits = false;
while (!has_credits) {
{
std::lock_guard lock(host_thread_ctx.cap.mutex);
if (host_thread_ctx.cap.credits > 0) {
host_thread_ctx.cap.credits--;
has_credits = true;
}
}
if (!has_credits) {
pw::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
std::array<uint8_t, 11> host_packet_arr = {0x02,
0x23,
0x01,
0x06,
0x00,
0x02,
0x00,
0x01,
0x00,
0, // Thread idx (unused)
static_cast<uint8_t>(j)};
host_thread_ctx.proxy.HandleH4HciFromHost(
H4PacketWithH4(host_packet_arr));
}
});
// Controller thread (sends NOCPs)
struct ControllerCtx {
uint16_t test_handle;
ProxyHost& proxy;
decltype(capture)& cap;
DynamicCreditSharingTest* test;
} controller_ctx{kTestHandle, proxy, capture, this};
threads.emplace_back(context.options(), [&controller_ctx]() {
while (!controller_ctx.cap.stop_controller) {
pw::this_thread::sleep_for(std::chrono::milliseconds(1));
bool send_nocp = false;
{
std::lock_guard lock(controller_ctx.cap.mutex);
if (controller_ctx.cap.nocp_sent < controller_ctx.cap.sequence.size()) {
controller_ctx.cap.nocp_sent++;
send_nocp = true;
}
}
if (send_nocp) {
(void)controller_ctx.test->SendNumberOfCompletedPackets(
controller_ctx.proxy, {{controller_ctx.test_handle, 1}});
}
}
});
// Wait for host thread to finish sending all packets.
threads[0].join();
// Stop controller thread.
capture.stop_controller = true;
threads[1].join();
JoinDispatcherThread();
// Verify ordering.
std::lock_guard lock(capture.mutex);
EXPECT_EQ(capture.sequence.size(), kPacketsPerThread);
for (size_t j = 1; j < capture.sequence.size(); ++j) {
EXPECT_GT(capture.sequence[j], capture.sequence[j - 1]);
}
}
TEST_F(DynamicCreditSharingTest, ReleaseFnDoesNotDeadlock) {
struct {
int sends_called = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&&) { capture.sends_called++; });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
*GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
// Set total credits to 1.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
constexpr uint16_t connection_handle = 0x123;
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, connection_handle, emboss::StatusCode::SUCCESS));
GattNotifyChannel channel =
BuildGattNotifyChannel(proxy, {.handle = connection_handle});
std::array<uint8_t, 1> attribute_value = {7};
// Send first packet (uses 1 credit).
{
multibuf::MultiBuf mbuf = MultiBufFromArray(attribute_value);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 1);
}
// Send second packet from host with a ReleaseFn that calls into proxy.
// It should be queued because credits are exhausted.
{
std::array<uint8_t, 9> host_packet_arr = {
0x02,
0x23,
0x01,
0x04,
0x00, // H4 Type, Connection Handle 0x123, Length = 4
0x00,
0x00,
0x01,
0x00 // L2CAP Header: Length = 0, CID = 1
};
// Create a packet with a ReleaseFn that calls GetNumFreeLeAclPackets
// TODO: b/505912880 - It would be better to call HandleH4HciFromHost
// instead of GetNumFreeLeAclPackets, but that currently deadlocks.
H4PacketWithH4 host_packet(host_packet_arr, [&proxy](const uint8_t*) {
// This calls into proxy and acquires credit_mutex_
(void)proxy.GetNumFreeLeAclPackets();
});
proxy.HandleH4HciFromHost(std::move(host_packet));
EXPECT_EQ(capture.sends_called, 1); // Still 1, packet is queued
}
// Reclaim 1 credit. This will trigger DrainDynamicQuota and dequeue the host
// packet. If the packet is sent or dropped, its ReleaseFn will be called. If
// called under locks, it will deadlock.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{connection_handle, 1}}));
// The queued packet should be sent out.
EXPECT_EQ(capture.sends_called, 2);
}
} // namespace
} // namespace pw::bluetooth::proxy