| // Copyright 2024 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 "pw_bluetooth_proxy/proxy_host.h" |
| |
| #include <cstdint> |
| #include <vector> |
| |
| #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_private/test_utils.h" |
| #include "pw_containers/flat_map.h" |
| #include "pw_function/function.h" |
| #include "pw_log/log.h" |
| #include "pw_span/span.h" |
| #include "pw_status/status.h" |
| #include "pw_unit_test/framework.h" |
| #include "pw_unit_test/status_macros.h" |
| |
| namespace pw::bluetooth::proxy { |
| |
| namespace { |
| |
| using containers::FlatMap; |
| |
| // Return a populated H4 command buffer of a type that proxy host doesn't |
| // interact with. |
| Status PopulateNoninteractingToControllerBuffer(H4PacketWithH4& h4_packet) { |
| return CreateAndPopulateToControllerView<emboss::InquiryCommandWriter>( |
| h4_packet, |
| emboss::OpCode::LINK_KEY_REQUEST_REPLY, |
| /*parameter_total_size=*/0) |
| .status(); |
| } |
| |
| // Return a populated H4 event buffer of a type that proxy host doesn't interact |
| // with. |
| Status CreateNonInteractingToHostBuffer(H4PacketWithHci& h4_packet) { |
| return CreateAndPopulateToHostEventWriter<emboss::InquiryCompleteEventWriter>( |
| h4_packet, emboss::EventCode::INQUIRY_COMPLETE) |
| .status(); |
| } |
| |
| // ########## Examples |
| |
| // Example for docs.rst. |
| TEST(Example, ExampleUsage) { |
| // Populate H4 buffer to send towards controller. |
| std::array<uint8_t, emboss::InquiryCommandView::SizeInBytes() + 1> |
| h4_array_from_host{}; |
| H4PacketWithH4 h4_packet_from_host{emboss::H4PacketType::UNKNOWN, |
| h4_array_from_host}; |
| PW_TEST_EXPECT_OK( |
| PopulateNoninteractingToControllerBuffer(h4_packet_from_host)); |
| |
| // Populate H4 buffer to send towards host. |
| std::array<uint8_t, emboss::InquiryCompleteEventView::SizeInBytes() + 1> |
| hci_array_from_controller{}; |
| H4PacketWithHci h4_packet_from_controller{emboss::H4PacketType::UNKNOWN, |
| hci_array_from_controller}; |
| |
| PW_TEST_EXPECT_OK( |
| CreateNonInteractingToHostBuffer(h4_packet_from_controller)); |
| |
| pw::Function<void(H4PacketWithHci && packet)> container_send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> container_send_to_controller_fn( |
| ([]([[maybe_unused]] H4PacketWithH4&& packet) {})); |
| |
| // DOCSTAG: [pw_bluetooth_proxy-examples-basic] |
| |
| #include "pw_bluetooth_proxy/proxy_host.h" |
| |
| // Container creates ProxyHost . |
| ProxyHost proxy = ProxyHost(std::move(container_send_to_host_fn), |
| std::move(container_send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| // Container passes H4 packets from host through proxy. Proxy will in turn |
| // call the container-provided `container_send_to_controller_fn` to pass them |
| // on to the controller. Some packets may be modified, added, or removed. |
| proxy.HandleH4HciFromHost(std::move(h4_packet_from_host)); |
| |
| // Container passes H4 packets from controller through proxy. Proxy will in |
| // turn call the container-provided `container_send_to_host_fn` to pass them |
| // on to the controller. Some packets may be modified, added, or removed. |
| proxy.HandleH4HciFromController(std::move(h4_packet_from_controller)); |
| |
| // DOCSTAG: [pw_bluetooth_proxy-examples-basic] |
| } |
| |
| // ########## PassthroughTest |
| |
| class PassthroughTest : public ProxyHostTest {}; |
| |
| // Verify buffer is properly passed (contents unaltered and zero-copy). |
| TEST_F(PassthroughTest, ToControllerPassesEqualBuffer) { |
| std::array<uint8_t, emboss::InquiryCommandView::SizeInBytes() + 1> h4_arr{}; |
| H4PacketWithH4 h4_packet{emboss::H4PacketType::UNKNOWN, h4_arr}; |
| PW_TEST_EXPECT_OK(PopulateNoninteractingToControllerBuffer(h4_packet)); |
| |
| // Struct for capturing because `pw::Function` can't fit multiple captures. |
| struct { |
| // Use a copy for comparison to catch if proxy incorrectly changes the |
| // passed buffer. |
| std::array<uint8_t, emboss::InquiryCommandView::SizeInBytes() + 1> h4_arr; |
| H4PacketWithH4* h4_packet; |
| uint8_t sends_called; |
| } send_capture = {h4_arr, &h4_packet, 0}; |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&send_capture](H4PacketWithH4&& packet) { |
| send_capture.sends_called++; |
| EXPECT_EQ(packet.GetH4Type(), |
| emboss::H4PacketType(send_capture.h4_arr[0])); |
| EXPECT_TRUE(std::equal(send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end(), |
| send_capture.h4_arr.begin() + 1, |
| send_capture.h4_arr.end())); |
| // Verify no copy by verifying buffer is at the same memory location. |
| EXPECT_EQ(packet.GetHciSpan().data(), |
| send_capture.h4_packet->GetHciSpan().data()); |
| }); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromHost(std::move(h4_packet)); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(send_capture.sends_called, 1); |
| } |
| |
| // Verify buffer is properly passed (contents unaltered and zero-copy). |
| TEST_F(PassthroughTest, ToHostPassesEqualBuffer) { |
| std::array<uint8_t, emboss::InquiryCompleteEventView::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_EXPECT_OK(CreateNonInteractingToHostBuffer(h4_packet)); |
| |
| // Struct for capturing because `pw::Function` can't fit multiple captures. |
| struct { |
| // Use a copy for comparison to catch if proxy incorrectly changes the |
| // passed buffer. |
| std::array<uint8_t, emboss::InquiryCompleteEventView::SizeInBytes()> |
| hci_arr; |
| H4PacketWithHci* h4_packet; |
| uint8_t sends_called; |
| } send_capture = {hci_arr, &h4_packet, 0}; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&send_capture](H4PacketWithHci&& packet) { |
| send_capture.sends_called++; |
| EXPECT_EQ(packet.GetH4Type(), send_capture.h4_packet->GetH4Type()); |
| EXPECT_TRUE(std::equal(send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end(), |
| send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end())); |
| // Verify no copy by verifying buffer is at the same memory location. |
| EXPECT_EQ(packet.GetHciSpan().data(), |
| send_capture.h4_packet->GetHciSpan().data()); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(send_capture.sends_called, 1); |
| } |
| |
| // Verify a command complete event (of a type that proxy doesn't act on) is |
| // properly passed (contents unaltered and zero-copy). |
| TEST_F(PassthroughTest, ToHostPassesEqualCommandComplete) { |
| std::array< |
| uint8_t, |
| emboss::ReadLocalVersionInfoCommandCompleteEventWriter::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::ReadLocalVersionInfoCommandCompleteEventWriter>( |
| h4_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::READ_LOCAL_VERSION_INFO); |
| |
| // Struct for capturing because `pw::Function` can't fit multiple captures. |
| struct { |
| std::array< |
| uint8_t, |
| emboss::ReadLocalVersionInfoCommandCompleteEventWriter::SizeInBytes()> |
| hci_arr; |
| H4PacketWithHci* h4_packet; |
| uint8_t sends_called; |
| } send_capture = {hci_arr, &h4_packet, 0}; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&send_capture](H4PacketWithHci&& packet) { |
| send_capture.sends_called++; |
| EXPECT_EQ(packet.GetH4Type(), send_capture.h4_packet->GetH4Type()); |
| EXPECT_TRUE(std::equal(send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end(), |
| send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end())); |
| // Verify no copy by verifying buffer is at the same memory location. |
| EXPECT_EQ(packet.GetHciSpan().data(), |
| send_capture.h4_packet->GetHciSpan().data()); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(send_capture.sends_called, 1); |
| } |
| |
| // ########## BadPacketTest |
| // The proxy should not affect buffers it can't process (it should just pass |
| // them on). |
| |
| class BadPacketTest : public ProxyHostTest {}; |
| |
| TEST_F(BadPacketTest, BadH4TypeToControllerIsPassedOn) { |
| std::array<uint8_t, emboss::InquiryCommandView::SizeInBytes() + 1> h4_arr{}; |
| H4PacketWithH4 h4_packet{emboss::H4PacketType::UNKNOWN, h4_arr}; |
| PW_TEST_EXPECT_OK(PopulateNoninteractingToControllerBuffer(h4_packet)); |
| // Set back to an invalid type (after |
| // PopulateNoninteractingToControllerBuffer). |
| h4_packet.SetH4Type(emboss::H4PacketType::UNKNOWN); |
| |
| // Struct for capturing because `pw::Function` can't fit multiple captures. |
| struct { |
| // Use a copy for comparison to catch if proxy incorrectly changes the |
| // passed buffer. |
| std::array<uint8_t, emboss::InquiryCommandView::SizeInBytes() + 1> h4_arr; |
| H4PacketWithH4* h4_packet; |
| uint8_t sends_called; |
| } send_capture = {h4_arr, &h4_packet, 0}; |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&send_capture](H4PacketWithH4&& packet) { |
| send_capture.sends_called++; |
| EXPECT_EQ(packet.GetH4Type(), |
| emboss::H4PacketType(send_capture.h4_arr[0])); |
| EXPECT_TRUE(std::equal(send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end(), |
| send_capture.h4_arr.begin() + 1, |
| send_capture.h4_arr.end())); |
| // Verify no copy by verifying buffer is at the same memory location. |
| EXPECT_EQ(packet.GetHciSpan().data(), |
| send_capture.h4_packet->GetHciSpan().data()); |
| }); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromHost(std::move(h4_packet)); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(send_capture.sends_called, 1); |
| } |
| |
| TEST_F(BadPacketTest, BadH4TypeToHostIsPassedOn) { |
| std::array<uint8_t, emboss::InquiryCompleteEventView::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_EXPECT_OK(CreateNonInteractingToHostBuffer(h4_packet)); |
| |
| // Set back to an invalid type. |
| h4_packet.SetH4Type(emboss::H4PacketType::UNKNOWN); |
| |
| // Struct for capturing because `pw::Function` can't fit multiple captures. |
| struct { |
| // Use a copy for comparison to catch if proxy incorrectly changes the |
| // passed buffer. |
| std::array<uint8_t, emboss::InquiryCompleteEventView::SizeInBytes()> |
| hci_arr; |
| H4PacketWithHci* h4_packet; |
| uint8_t sends_called = 0; |
| } send_capture = {hci_arr, &h4_packet, 0}; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&send_capture](H4PacketWithHci&& packet) { |
| send_capture.sends_called++; |
| EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::UNKNOWN); |
| EXPECT_TRUE(std::equal(send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end(), |
| send_capture.h4_packet->GetHciSpan().begin(), |
| send_capture.h4_packet->GetHciSpan().end())); |
| // Verify no copy by verifying buffer is at the same memory location. |
| EXPECT_EQ(packet.GetHciSpan().data(), |
| send_capture.h4_packet->GetHciSpan().data()); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(send_capture.sends_called, 1); |
| } |
| |
| TEST_F(BadPacketTest, EmptyBufferToControllerIsPassedOn) { |
| std::array<uint8_t, 0> h4_arr; |
| H4PacketWithH4 h4_packet{emboss::H4PacketType::COMMAND, h4_arr}; |
| // H4PacketWithH4 use the underlying h4 buffer to store type. Since its length |
| // is zero, it can't store it and will always return UNKNOWN. |
| EXPECT_EQ(h4_packet.GetH4Type(), emboss::H4PacketType::UNKNOWN); |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&sends_called](H4PacketWithH4&& packet) { |
| sends_called++; |
| EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::UNKNOWN); |
| EXPECT_TRUE(packet.GetHciSpan().empty()); |
| }); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromHost(std::move(h4_packet)); |
| |
| // Verify callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| TEST_F(BadPacketTest, EmptyBufferToHostIsPassedOn) { |
| std::array<uint8_t, 0> hci_arr; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::EVENT, hci_arr}; |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&sends_called](H4PacketWithHci&& packet) { |
| sends_called++; |
| EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::EVENT); |
| EXPECT_TRUE(packet.GetHciSpan().empty()); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| // Verify callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| TEST_F(BadPacketTest, TooShortEventToHostIsPassOn) { |
| std::array<uint8_t, emboss::InquiryCompleteEventView::SizeInBytes()> |
| valid_hci_arr{}; |
| H4PacketWithHci valid_packet{emboss::H4PacketType::UNKNOWN, valid_hci_arr}; |
| PW_TEST_EXPECT_OK(CreateNonInteractingToHostBuffer(valid_packet)); |
| |
| // Create packet for sending whose span size is one less than a valid command |
| // complete event. |
| H4PacketWithHci h4_packet{valid_packet.GetH4Type(), |
| valid_packet.GetHciSpan().subspan( |
| 0, emboss::EventHeaderView::SizeInBytes() - 1)}; |
| |
| // Struct for capturing because `pw::Function` can't fit multiple captures. |
| struct { |
| std::array<uint8_t, emboss::EventHeaderView::SizeInBytes() - 1> hci_arr; |
| uint8_t sends_called = 0; |
| } send_capture; |
| // Copy valid event into a short_array whose size is one less than a valid |
| // EventHeader. |
| std::copy(h4_packet.GetHciSpan().begin(), |
| h4_packet.GetHciSpan().end(), |
| send_capture.hci_arr.begin()); |
| send_capture.sends_called = 0; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&send_capture](H4PacketWithHci&& packet) { |
| send_capture.sends_called++; |
| EXPECT_TRUE(std::equal(packet.GetHciSpan().begin(), |
| packet.GetHciSpan().end(), |
| send_capture.hci_arr.begin(), |
| send_capture.hci_arr.end())); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| // Verify callback was called. |
| EXPECT_EQ(send_capture.sends_called, 1); |
| } |
| |
| TEST_F(BadPacketTest, TooShortCommandCompleteEventToHost) { |
| std::array< |
| uint8_t, |
| emboss::ReadLocalVersionInfoCommandCompleteEventWriter::SizeInBytes()> |
| valid_hci_arr{}; |
| H4PacketWithHci valid_packet{emboss::H4PacketType::UNKNOWN, valid_hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::ReadLocalVersionInfoCommandCompleteEventWriter>( |
| valid_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::READ_LOCAL_VERSION_INFO); |
| |
| // Create packet for sending whose span size is one less than a valid command |
| // complete event. |
| H4PacketWithHci h4_packet{ |
| valid_packet.GetH4Type(), |
| valid_packet.GetHciSpan().subspan( |
| 0, |
| emboss::ReadLocalVersionInfoCommandCompleteEventWriter:: |
| SizeInBytes() - |
| 1)}; |
| |
| // Struct for capturing because `pw::Function` capture can't fit multiple |
| // fields . |
| struct { |
| std::array< |
| uint8_t, |
| emboss::ReadLocalVersionInfoCommandCompleteEventWriter::SizeInBytes() - |
| 1> |
| hci_arr; |
| uint8_t sends_called = 0; |
| } send_capture; |
| std::copy(h4_packet.GetHciSpan().begin(), |
| h4_packet.GetHciSpan().end(), |
| send_capture.hci_arr.begin()); |
| send_capture.sends_called = 0; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&send_capture](H4PacketWithHci&& packet) { |
| send_capture.sends_called++; |
| EXPECT_TRUE(std::equal(packet.GetHciSpan().begin(), |
| packet.GetHciSpan().end(), |
| send_capture.hci_arr.begin(), |
| send_capture.hci_arr.end())); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| // Verify callback was called. |
| EXPECT_EQ(send_capture.sends_called, 1); |
| } |
| |
| // ########## ReserveLeAclCreditsTest |
| |
| class ReserveLeAclCreditsTest : public ProxyHostTest {}; |
| |
| // Proxy Host should reserve requested ACL credits from controller's ACL credits |
| // when using ReadBufferSize command. |
| TEST_F(ReserveLeAclCreditsTest, ProxyCreditsReserveCreditsWithReadBufferSize) { |
| std::array<uint8_t, |
| emboss::ReadBufferSizeCommandCompleteEventWriter::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::ReadBufferSizeCommandCompleteEventWriter>( |
| h4_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::READ_BUFFER_SIZE); |
| view.total_num_acl_data_packets().Write(10); |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&sends_called](H4PacketWithHci&& received_packet) { |
| sends_called++; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_view, |
| MakeEmbossWriter<emboss::ReadBufferSizeCommandCompleteEventWriter>( |
| received_packet.GetHciSpan())); |
| // Should reserve 2 credits from original total of 10 (so 8 left for |
| // host). |
| EXPECT_EQ(event_view.total_num_acl_data_packets().Read(), 8); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/2); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(proxy.GetNumFreeBrEdrAclPackets(), 2); |
| |
| EXPECT_TRUE(proxy.HasSendBrEdrAclCapability()); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| // Proxy Host should reserve requested ACL LE credits from controller's ACL LE |
| // credits when using LEReadBufferSizeV1 command. |
| TEST_F(ReserveLeAclCreditsTest, |
| ProxyCreditsReserveCreditsWithLEReadBufferSizeV1) { |
| std::array< |
| uint8_t, |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| h4_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::LE_READ_BUFFER_SIZE_V1); |
| view.total_num_le_acl_data_packets().Write(10); |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&sends_called](H4PacketWithHci&& received_packet) { |
| sends_called++; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_view, |
| MakeEmbossView< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| received_packet.GetHciSpan())); |
| |
| // Should reserve 2 credits from original total of 10 (so 8 left for |
| // host). |
| EXPECT_EQ(event_view.total_num_le_acl_data_packets().Read(), 8); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| |
| EXPECT_TRUE(proxy.HasSendLeAclCapability()); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| // Proxy Host should reserve requested ACL LE credits from controller's ACL LE |
| // credits when using LEReadBufferSizeV2 command. |
| TEST_F(ReserveLeAclCreditsTest, |
| ProxyCreditsReserveCreditsWithLEReadBufferSizeV2) { |
| std::array< |
| uint8_t, |
| emboss::LEReadBufferSizeV2CommandCompleteEventWriter::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::LEReadBufferSizeV2CommandCompleteEventWriter>( |
| h4_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::LE_READ_BUFFER_SIZE_V2); |
| view.total_num_le_acl_data_packets().Write(10); |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&sends_called](H4PacketWithHci&& received_packet) { |
| sends_called++; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_view, |
| MakeEmbossView< |
| emboss::LEReadBufferSizeV2CommandCompleteEventWriter>( |
| received_packet.GetHciSpan())); |
| // Should reserve 2 credits from original total of 10 (so 8 left for |
| // host). |
| EXPECT_EQ(event_view.total_num_le_acl_data_packets().Read(), 8); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| |
| EXPECT_TRUE(proxy.HasSendLeAclCapability()); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| // If controller provides less than wanted credits, we should reserve that |
| // smaller amount. |
| TEST_F(ReserveLeAclCreditsTest, ProxyCreditsCappedByControllerCredits) { |
| std::array< |
| uint8_t, |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| h4_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::LE_READ_BUFFER_SIZE_V1); |
| view.total_num_le_acl_data_packets().Write(5); |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&sends_called](H4PacketWithHci&& received_packet) { |
| sends_called++; |
| // We want 7, but can reserve only 5 from original 5 (so 0 left for |
| // host). |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_view, |
| MakeEmbossView< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| received_packet.GetHciSpan())); |
| EXPECT_EQ(event_view.total_num_le_acl_data_packets().Read(), 0); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/7, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 5); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| // Proxy Host can reserve zero credits from controller's ACL LE credits. |
| TEST_F(ReserveLeAclCreditsTest, ProxyCreditsReserveZeroCredits) { |
| std::array< |
| uint8_t, |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| h4_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::LE_READ_BUFFER_SIZE_V1); |
| view.total_num_le_acl_data_packets().Write(10); |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&sends_called](H4PacketWithHci&& received_packet) { |
| sends_called++; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_view, |
| MakeEmbossView< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| received_packet.GetHciSpan())); |
| // Should reserve 0 credits from original total of 10 (so 10 left for |
| // host). |
| EXPECT_EQ(event_view.total_num_le_acl_data_packets().Read(), 10); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| |
| EXPECT_FALSE(proxy.HasSendLeAclCapability()); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| // If controller has no credits, proxy should reserve none. |
| TEST_F(ReserveLeAclCreditsTest, ProxyCreditsZeroWhenHostCreditsZero) { |
| std::array< |
| uint8_t, |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter::SizeInBytes()> |
| hci_arr{}; |
| H4PacketWithHci h4_packet{emboss::H4PacketType::UNKNOWN, hci_arr}; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| CreateAndPopulateToHostEventWriter< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| h4_packet, emboss::EventCode::COMMAND_COMPLETE)); |
| view.command_complete().command_opcode().Write( |
| emboss::OpCode::LE_READ_BUFFER_SIZE_V1); |
| view.total_num_le_acl_data_packets().Write(0); |
| |
| uint8_t sends_called = 0; |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&sends_called](H4PacketWithHci&& received_packet) { |
| sends_called++; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_view, |
| MakeEmbossView< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| received_packet.GetHciSpan())); |
| // Should reserve 0 credit from original total of 0 (so 0 left for |
| // host). |
| EXPECT_EQ(event_view.total_num_le_acl_data_packets().Read(), 0); |
| }); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| |
| EXPECT_TRUE(proxy.HasSendLeAclCapability()); |
| |
| // Verify to controller callback was called. |
| EXPECT_EQ(sends_called, 1); |
| } |
| |
| TEST_F(ReserveLeAclCreditsTest, ProxyCreditsZeroWhenNotInitialized) { |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| |
| EXPECT_TRUE(proxy.HasSendLeAclCapability()); |
| } |
| |
| // ########## NumberOfCompletedPacketsTest |
| |
| class NumberOfCompletedPacketsTest : public ProxyHostTest {}; |
| |
| TEST_F(NumberOfCompletedPacketsTest, TwoOfThreeSentPacketsComplete) { |
| constexpr size_t kNumConnections = 3; |
| struct { |
| int sends_called = 0; |
| const std::array<uint16_t, kNumConnections> connection_handles = { |
| 0x123, 0x456, 0x789}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| EXPECT_EQ(packet.GetHciSpan().size(), 15ul); |
| EXPECT_EQ(view.num_handles().Read(), capture.connection_handles.size()); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Proxy should have reclaimed 1 credit from Connection 0 (leaving 0 |
| // credits in packet), no credits from Connection 1 (meaning 0 will be |
| // unchanged), and 1 credit from Connection 2 (leaving 0). |
| EXPECT_EQ(view.nocp_data()[0].connection_handle().Read(), |
| capture.connection_handles[0]); |
| EXPECT_EQ(view.nocp_data()[0].num_completed_packets().Read(), 0); |
| |
| EXPECT_EQ(view.nocp_data()[1].connection_handle().Read(), |
| capture.connection_handles[1]); |
| EXPECT_EQ(view.nocp_data()[1].num_completed_packets().Read(), 0); |
| |
| EXPECT_EQ(view.nocp_data()[2].connection_handle().Read(), |
| capture.connection_handles[2]); |
| EXPECT_EQ(view.nocp_data()[2].num_completed_packets().Read(), 0); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/kNumConnections, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK( |
| SendLeReadBufferResponseFromController(proxy, kNumConnections)); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| std::array<uint8_t, 1> attribute_value = {7}; |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 3); |
| |
| // Send packet; num free packets should decrement. |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = capture.connection_handles[0]}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| // Proxy host took all credits so will not pass NOCP on to host. |
| EXPECT_EQ(capture.sends_called, 1); |
| } |
| |
| // Send packet over Connection 1, which will not have a packet completed in |
| // the Number_of_Completed_Packets event. |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = capture.connection_handles[1]}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 1); |
| } |
| |
| // Send third packet; num free packets should decrement again. |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = capture.connection_handles[2]}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| } |
| |
| // Send Number_of_Completed_Packets event that reports 1 packet on Connection |
| // 0, 0 packets on Connection 1, and 1 packet on Connection 2. Checks in |
| // send_to_host_fn will ensure we have reclaimed 2 of 3 credits. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 3>({{{capture.connection_handles[0], 1}, |
| {capture.connection_handles[1], 0}, |
| {capture.connection_handles[2], 1}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| // Proxy host took all credits so will not pass NOCP event on to host. |
| EXPECT_EQ(capture.sends_called, 1); |
| } |
| |
| TEST_F(NumberOfCompletedPacketsTest, |
| ManyMorePacketsCompletedThanPacketsPending) { |
| constexpr size_t kNumConnections = 2; |
| struct { |
| int sends_called = 0; |
| const std::array<uint16_t, kNumConnections> connection_handles = {0x123, |
| 0x456}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| EXPECT_EQ(packet.GetHciSpan().size(), 11ul); |
| EXPECT_EQ(view.num_handles().Read(), capture.connection_handles.size()); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Proxy should have reclaimed 1 credit from Connection 0 (leaving |
| // 9 credits in packet) and 1 credit from Connection 2 (leaving 14). |
| EXPECT_EQ(view.nocp_data()[0].connection_handle().Read(), |
| capture.connection_handles[0]); |
| EXPECT_EQ(view.nocp_data()[0].num_completed_packets().Read(), 9); |
| |
| EXPECT_EQ(view.nocp_data()[1].connection_handle().Read(), |
| capture.connection_handles[1]); |
| EXPECT_EQ(view.nocp_data()[1].num_completed_packets().Read(), 14); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2)); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| |
| // Send packet over Connection 0; num free packets should decrement. |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = capture.connection_handles[0]}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 1); |
| } |
| |
| // Send packet over Connection 1; num free packets should decrement again. |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = capture.connection_handles[1]}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| } |
| |
| // Send Number_of_Completed_Packets event that reports 10 packets on |
| // Connection 0 and 15 packets on Connection 1. Checks in send_to_host_fn |
| // will ensure we have reclaimed exactly 2 credits, 1 from each Connection. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 2>({{{capture.connection_handles[0], 10}, |
| {capture.connection_handles[1], 15}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| EXPECT_EQ(capture.sends_called, 2); |
| } |
| |
| TEST_F(NumberOfCompletedPacketsTest, ProxyReclaimsOnlyItsUsedCredits) { |
| constexpr size_t kNumConnections = 2; |
| struct { |
| int sends_called = 0; |
| const std::array<uint16_t, kNumConnections> connection_handles = {0x123, |
| 0x456}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| EXPECT_EQ(packet.GetHciSpan().size(), 11ul); |
| EXPECT_EQ(view.num_handles().Read(), 2); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Proxy has 4 credits it wants to reclaim, but it should have only |
| // reclaimed the 2 credits it used on Connection 0. |
| EXPECT_EQ(view.nocp_data()[0].connection_handle().Read(), |
| capture.connection_handles[0]); |
| EXPECT_EQ(view.nocp_data()[0].num_completed_packets().Read(), 8); |
| EXPECT_EQ(view.nocp_data()[1].connection_handle().Read(), |
| capture.connection_handles[1]); |
| EXPECT_EQ(view.nocp_data()[1].num_completed_packets().Read(), 15); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/4, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 4)); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| |
| // Use 2 credits on Connection 0 and 2 credits on random connections that will |
| // not be included in the NOCP event. |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = capture.connection_handles[0]}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| } |
| { |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = 0xABC}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| } |
| |
| // Send Number_of_Completed_Packets event that reports 10 packets on |
| // Connection 0 and 15 packets on Connection 1. Checks in send_to_host_fn |
| // will ensure we have reclaimed only 2 credits. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 2>({{{capture.connection_handles[0], 10}, |
| {capture.connection_handles[1], 15}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| // NOCP has credits remaining so will be passed on to host. |
| EXPECT_EQ(capture.sends_called, 2); |
| } |
| |
| TEST_F(NumberOfCompletedPacketsTest, EventUnmodifiedIfNoCreditsInUse) { |
| constexpr size_t kNumConnections = 2; |
| struct { |
| int sends_called = 0; |
| const std::array<uint16_t, kNumConnections> connection_handles = {0x123, |
| 0x456}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| EXPECT_EQ(packet.GetHciSpan().size(), 11ul); |
| EXPECT_EQ(view.num_handles().Read(), 2); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Event should be unmodified. |
| EXPECT_EQ(view.nocp_data()[0].connection_handle().Read(), |
| capture.connection_handles[0]); |
| EXPECT_EQ(view.nocp_data()[0].num_completed_packets().Read(), 10); |
| EXPECT_EQ(view.nocp_data()[1].connection_handle().Read(), |
| capture.connection_handles[1]); |
| EXPECT_EQ(view.nocp_data()[1].num_completed_packets().Read(), 15); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/10, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 10)); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| // Send Number_of_Completed_Packets event that reports 10 packets on |
| // Connection 0 and 15 packets on Connection 1. Checks in send_to_host_fn |
| // will ensure we have not modified the NOCP event. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 2>({{{capture.connection_handles[0], 10}, |
| {capture.connection_handles[1], 15}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 10); |
| // NOCP has credits remaining so will be passed on to host. |
| EXPECT_EQ(capture.sends_called, 2); |
| } |
| |
| TEST_F(NumberOfCompletedPacketsTest, HandlesUnusualEvents) { |
| constexpr size_t kNumConnections = 5; |
| struct { |
| int sends_called = 0; |
| const std::array<uint16_t, kNumConnections> connection_handles = { |
| 0x123, 0x234, 0x345, 0x456, 0x567}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| if (view.num_handles().Read() == 0) { |
| return; |
| } |
| |
| EXPECT_EQ(packet.GetHciSpan().size(), 23ul); |
| EXPECT_EQ(view.num_handles().Read(), 5); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Event should be unmodified. |
| for (int i = 0; i < 5; ++i) { |
| EXPECT_EQ(view.nocp_data()[i].connection_handle().Read(), |
| capture.connection_handles[i]); |
| EXPECT_EQ(view.nocp_data()[i].num_completed_packets().Read(), 0); |
| } |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/10, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 10)); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| // Send Number_of_Completed_Packets event with no entries. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, FlatMap<uint16_t, uint16_t, 0>({{}}))); |
| // NOCP has no entries, so will not be passed on to host. |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| // Send Number_of_Completed_Packets event that reports 0 packets for various |
| // connections. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 5>({{{capture.connection_handles[0], 0}, |
| {capture.connection_handles[1], 0}, |
| {capture.connection_handles[2], 0}, |
| {capture.connection_handles[3], 0}, |
| {capture.connection_handles[4], 0}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 10); |
| // Proxy host will not pass on a NOCP with no credits. |
| EXPECT_EQ(capture.sends_called, 1); |
| } |
| |
| TEST_F(NumberOfCompletedPacketsTest, MultipleChannelsDifferentTransports) { |
| static constexpr size_t kPayloadSize = 3; |
| struct { |
| int sends_called = 0; |
| std::array<uint8_t, kPayloadSize> payload = { |
| 0xAB, |
| 0xCD, |
| 0xEF, |
| }; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci&&)>&& send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| pw::Function<void(H4PacketWithH4&&)>&& send_to_controller_fn( |
| [&capture](H4PacketWithH4&&) { ++capture.sends_called; }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/1); |
| // Allow proxy to reserve BR/EDR 1 credit. |
| PW_TEST_EXPECT_OK(SendReadBufferResponseFromController(proxy, 1)); |
| // Allow proxy to reserve LE 1 credit. |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| |
| // Test that sending on one type of transport doesn't get blocked if the other |
| // type of transport is out of credits. |
| |
| L2capCoc le_channel = |
| BuildCoc(proxy, CocParameters{.handle = 0x123, .tx_credits = 2}); |
| PW_TEST_EXPECT_OK(le_channel.Write(multibuf::MultiBuf{}).status); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| RfcommChannel bredr_channel = |
| BuildRfcomm(proxy, RfcommParameters{.handle = 0x456}); |
| PW_TEST_EXPECT_OK( |
| bredr_channel.Write(MultiBufFromSpan(pw::span(capture.payload))).status); |
| // Send should succeed even though no LE credits available |
| EXPECT_EQ(capture.sends_called, 2); |
| |
| // Queue an LE write |
| PW_TEST_EXPECT_OK(le_channel.Write(multibuf::MultiBuf{}).status); |
| EXPECT_EQ(capture.sends_called, 2); |
| |
| // Complete previous LE write |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, FlatMap<uint16_t, uint16_t, 1>({{{0x123, 1}}}))); |
| EXPECT_EQ(capture.sends_called, 3); |
| |
| // Complete BR/EDR write |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, FlatMap<uint16_t, uint16_t, 1>({{{0x456, 1}}}))); |
| |
| // Write again |
| PW_TEST_EXPECT_OK( |
| bredr_channel.Write(MultiBufFromSpan(pw::span(capture.payload))).status); |
| EXPECT_EQ(capture.sends_called, 4); |
| } |
| |
| // ########## DisconnectionCompleteTest |
| |
| class DisconnectionCompleteTest : public ProxyHostTest {}; |
| |
| TEST_F(DisconnectionCompleteTest, DisconnectionReclaimsCredits) { |
| struct { |
| int sends_called = 0; |
| uint16_t connection_handle = 0x123; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| EXPECT_EQ(packet.GetHciSpan().size(), 7ul); |
| EXPECT_EQ(view.num_handles().Read(), 1); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Event should be unmodified. |
| EXPECT_EQ(view.nocp_data()[0].connection_handle().Read(), |
| capture.connection_handle); |
| EXPECT_EQ(view.nocp_data()[0].num_completed_packets().Read(), 10); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/10, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 10)); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| |
| { |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = capture.connection_handle}); |
| |
| // Use up 3 of the 10 credits on the Connection that will be disconnected. |
| for (int i = 0; i < 3; ++i) { |
| EXPECT_TRUE( |
| channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| } |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 7); |
| } |
| |
| // Use up 2 credits on a random Connection. |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {}); |
| |
| for (int i = 0; i < 2; ++i) { |
| EXPECT_TRUE( |
| channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| } |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 5); |
| } |
| |
| // Send Disconnection_Complete event, which should reclaim 3 credits. |
| PW_TEST_EXPECT_OK( |
| SendDisconnectionCompleteEvent(proxy, capture.connection_handle)); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 8); |
| |
| // Use 1 credit and reclaim it on a bunch of random channels. Then send |
| // disconnect and ensure it was cleaned up in connections list. The send will |
| // fail if disconnect doesn't cleanup properly. |
| // |
| // We already have an active connection at this point in the test, so loop |
| // over the remaining slots + 1 which would otherwise fail if cleanup wasn't |
| // working right. |
| for (uint16_t i = 0; i < ProxyHost::GetMaxNumAclConnections() - 2; ++i) { |
| uint16_t handle = 0x234 + i; |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = handle}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, FlatMap<uint16_t, uint16_t, 1>({{{handle, 1}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 8); |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, handle)); |
| } |
| |
| // Send Number_of_Completed_Packets event that reports 10 packets, none of |
| // which should be reclaimed because this Connection has disconnected. Checks |
| // in send_to_host_fn will ensure we have not modified the NOCP event. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 1>({{{capture.connection_handle, 10}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 8); |
| // NOCP has credits remaining so will be passed on to host. |
| EXPECT_EQ(capture.sends_called, 11); |
| } |
| |
| TEST_F(DisconnectionCompleteTest, FailedDisconnectionHasNoEffect) { |
| uint16_t connection_handle = 0x123; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| |
| // Use sole credit. |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = connection_handle}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| |
| // Send failed Disconnection_Complete event, should not reclaim credit. |
| PW_TEST_EXPECT_OK( |
| SendDisconnectionCompleteEvent(proxy, |
| connection_handle, |
| /*direction=*/Direction::kFromController, |
| /*successful=*/false)); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| } |
| |
| TEST_F(DisconnectionCompleteTest, DisconnectionOfUnusedConnectionHasNoEffect) { |
| uint16_t connection_handle = 0x123; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| |
| // Use sole credit. |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = connection_handle}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| |
| // Send Disconnection_Complete event to random Connection, should have no |
| // effect. |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, 0x456)); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| } |
| |
| TEST_F(DisconnectionCompleteTest, CanReuseConnectionHandleAfterDisconnection) { |
| struct { |
| int sends_called = 0; |
| uint16_t connection_handle = 0x123; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| EXPECT_EQ(packet.GetHciSpan().size(), 7ul); |
| EXPECT_EQ(view.num_handles().Read(), 1); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Should have reclaimed the 1 packet. |
| EXPECT_EQ(view.nocp_data()[0].connection_handle().Read(), |
| capture.connection_handle); |
| EXPECT_EQ(view.nocp_data()[0].num_completed_packets().Read(), 0); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| EXPECT_EQ(capture.sends_called, 1); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| |
| { |
| // Establish connection over `connection_handle`. |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = capture.connection_handle}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| } |
| |
| // Disconnect `connection_handle`. |
| PW_TEST_EXPECT_OK( |
| SendDisconnectionCompleteEvent(proxy, capture.connection_handle)); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 1); |
| EXPECT_EQ(capture.sends_called, 2); |
| |
| { |
| // Re-establish connection over `connection_handle`. |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = capture.connection_handle}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| } |
| |
| // Send Number_of_Completed_Packets event that reports 1 packet. Checks in |
| // send_to_host_fn will ensure packet has been reclaimed. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 1>({{{capture.connection_handle, 1}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 1); |
| // Since proxy reclaimed the one credit, it does not pass event on to host. |
| EXPECT_EQ(capture.sends_called, 2); |
| } |
| |
| TEST_F(DisconnectionCompleteTest, DisconnectionErasesAclConnection) { |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| int sends_called = 0; |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&sends_called](H4PacketWithH4&&) { ++sends_called; }); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| |
| uint16_t connection_handle = 0x567; |
| pw::Vector<L2capCoc, ProxyHost::GetMaxNumAclConnections()> channels; |
| for (size_t i = 0; i < ProxyHost::GetMaxNumAclConnections(); ++i) { |
| channels.push_back( |
| BuildCoc(proxy, CocParameters{.handle = ++connection_handle})); |
| } |
| EXPECT_EQ( |
| BuildCocWithResult( |
| proxy, |
| CocParameters{.handle = static_cast<uint16_t>(connection_handle + 1)}) |
| .status(), |
| Status::Unavailable()); |
| |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, connection_handle++)); |
| // After erasing the last ACL connection, there should be space for a new one. |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| L2capCoc channel, |
| BuildCocWithResult(proxy, CocParameters{.handle = connection_handle})); |
| // Confirm signaling channels are functional. |
| PW_TEST_EXPECT_OK(channel.SendAdditionalRxCredits(3)); |
| EXPECT_EQ(sends_called, 1); |
| channels.clear(); |
| } |
| |
| // ########## DestructionTest |
| |
| class DestructionTest : public ProxyHostTest {}; |
| |
| // This test can deadlock on failure. |
| TEST_F(DestructionTest, CanDestructWhenPacketsQueuedInSignalingChannel) { |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| L2capCoc channel = BuildCoc(proxy, CocParameters{.handle = 0x111}); |
| L2capCoc channel2 = BuildCoc(proxy, CocParameters{.handle = 0x222}); |
| |
| PW_TEST_EXPECT_OK(channel.SendAdditionalRxCredits(1)); |
| } |
| |
| // ########## ResetTest |
| |
| class ResetTest : public ProxyHostTest {}; |
| |
| TEST_F(ResetTest, ResetClearsActiveConnections) { |
| struct { |
| int sends_called = 0; |
| const uint16_t connection_handle = 0x123; |
| } host_capture; |
| struct { |
| int sends_called = 0; |
| const uint16_t connection_handle = 0x123; |
| } controller_capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&host_capture](H4PacketWithHci&& packet) { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto event_header, |
| MakeEmbossView<emboss::EventHeaderView>(packet.GetHciSpan().subspan( |
| 0, emboss::EventHeader::IntrinsicSizeInBytes()))); |
| host_capture.sends_called++; |
| if (event_header.event_code().Read() != |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS) { |
| return; |
| } |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto view, |
| MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>( |
| packet.GetHciSpan())); |
| EXPECT_EQ(packet.GetHciSpan().size(), 7ul); |
| EXPECT_EQ(view.num_handles().Read(), 1); |
| EXPECT_EQ(view.header().event_code().Read(), |
| emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS); |
| |
| // Should be unchanged. |
| EXPECT_EQ(view.nocp_data()[0].connection_handle().Read(), |
| host_capture.connection_handle); |
| EXPECT_EQ(view.nocp_data()[0].num_completed_packets().Read(), 1); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&controller_capture]([[maybe_unused]] H4PacketWithH4&& packet) { |
| ++controller_capture.sends_called; |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2)); |
| EXPECT_EQ(host_capture.sends_called, 1); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = controller_capture.connection_handle}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(controller_capture.sends_called, 1); |
| } |
| |
| proxy.Reset(); |
| |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| // Reset should not have cleared `le_acl_credits_to_reserve`, so proxy should |
| // still indicate the capability. |
| EXPECT_TRUE(proxy.HasSendLeAclCapability()); |
| |
| // Re-initialize AclDataChannel with 2 credits. |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2)); |
| EXPECT_EQ(host_capture.sends_called, 2); |
| |
| { |
| // Send ACL on random handle to expend one credit. |
| GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {}); |
| EXPECT_TRUE(channel.Write(MultiBufFromArray(attribute_value)).status.ok()); |
| EXPECT_EQ(controller_capture.sends_called, 2); |
| } |
| |
| // This should have no effect, as the reset has cleared our active connection |
| // on this handle. |
| PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets( |
| proxy, |
| FlatMap<uint16_t, uint16_t, 1>({{{host_capture.connection_handle, 1}}}))); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 1); |
| // NOCP has credits remaining so will be passed on to host. |
| EXPECT_EQ(host_capture.sends_called, 3); |
| } |
| |
| TEST_F(ResetTest, ProxyHandlesMultipleResets) { |
| int sends_called = 0; |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&sends_called]([[maybe_unused]] H4PacketWithH4&& packet) { |
| ++sends_called; |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| |
| proxy.Reset(); |
| proxy.Reset(); |
| |
| std::array<uint8_t, 1> attribute_value = {0}; |
| // Validate state after double reset. |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| EXPECT_TRUE(proxy.HasSendLeAclCapability()); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {}); |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| } |
| EXPECT_EQ(sends_called, 1); |
| |
| proxy.Reset(); |
| |
| // Validate state after third reset. |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0); |
| EXPECT_TRUE(proxy.HasSendLeAclCapability()); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| { |
| GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {}); |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| } |
| EXPECT_EQ(sends_called, 2); |
| } |
| |
| TEST_F(ResetTest, HandleHciReset) { |
| struct { |
| int sends_called = 0; |
| const uint16_t connection_handle = 0x123; |
| } host_capture; |
| struct { |
| int sends_called = 0; |
| const uint16_t connection_handle = 0x123; |
| } controller_capture; |
| |
| pw::Function<void(H4PacketWithHci&&)> send_to_host_fn( |
| [&host_capture](H4PacketWithHci&&) { ++host_capture.sends_called; }); |
| pw::Function<void(H4PacketWithH4&&)> send_to_controller_fn( |
| [&controller_capture](H4PacketWithH4&&) { |
| ++controller_capture.sends_called; |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/2, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2)); |
| EXPECT_EQ(host_capture.sends_called, 1); |
| |
| // Use 1 credit. |
| std::array<uint8_t, 1> attribute_value = {0}; |
| GattNotifyChannel channel = BuildGattNotifyChannel( |
| proxy, {.handle = controller_capture.connection_handle}); |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| EXPECT_EQ(controller_capture.sends_called, 1); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 1); |
| |
| // Send HCI_Reset. This should cause proxy to reset and our free credits as |
| // well. |
| std::array<uint8_t, emboss::ResetCommandView::SizeInBytes() + 1> |
| h4_array_from_host{}; |
| H4PacketWithH4 h4_packet_from_host{emboss::H4PacketType::UNKNOWN, |
| h4_array_from_host}; |
| PW_TEST_EXPECT_OK( |
| CreateAndPopulateToControllerView<emboss::ResetCommandWriter>( |
| h4_packet_from_host, |
| emboss::OpCode::RESET, |
| /*parameter_total_size=*/0)); |
| proxy.HandleH4HciFromHost(std::move(h4_packet_from_host)); |
| |
| // Send new buffer response which shouldn't crash. |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 2)); |
| EXPECT_EQ(host_capture.sends_called, 2); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 2); |
| } |
| |
| // ########## MultiSendTest |
| |
| class MultiSendTest : public ProxyHostTest {}; |
| |
| TEST_F(MultiSendTest, CanOccupyAllThenReuseEachBuffer) { |
| constexpr size_t kAclBuffersSize = |
| ProxyHost::GetNumSimultaneousAclSendsSupported(); |
| // Total number of expected sends for this test. |
| constexpr size_t kExpectedSendCount = (2 * kAclBuffersSize) + 1; |
| // We allocate some extra slots in case there is a bug (which will be caught |
| // by the test EXPECTs). |
| constexpr size_t kMaxSendCount = kExpectedSendCount + 5; |
| struct { |
| size_t sends_called = 0; |
| // These are packets that have been sent towards controller, but not |
| // released yet by container. |
| pw::Vector<H4PacketWithH4, kMaxSendCount> in_flight_packets{}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&capture](H4PacketWithH4&& packet) { |
| // Capture all packets to prevent their destruction. |
| capture.sends_called++; |
| capture.in_flight_packets.push_back(std::move(packet)); |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/kMaxSendCount, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK( |
| SendLeReadBufferResponseFromController(proxy, kMaxSendCount)); |
| |
| GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {}); |
| |
| std::array<uint8_t, 1> attribute_value = {0xF}; |
| |
| // Occupy all H4 buffers. |
| for (size_t sent = 1; sent <= kAclBuffersSize; ++sent) { |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| // Each write is sent towards controller |
| EXPECT_EQ(capture.sends_called, sent); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), kMaxSendCount - sent); |
| // Container holds on to each H4 buffer. |
| EXPECT_EQ(capture.in_flight_packets.size(), sent); |
| } |
| |
| // This was already verified in last iteration of loop above, but we EXPECT |
| // again to provide reader context for EXPECTs after the following Write. |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), kMaxSendCount - kAclBuffersSize); |
| EXPECT_EQ(capture.in_flight_packets.size(), kAclBuffersSize); |
| |
| // At this point all H4 buffers are in use. We can still write to channel, but |
| // those payloads will queue in the channel until H4 packets are freed. |
| PW_TEST_EXPECT_OK(channel.Write(MultiBufFromArray(attribute_value)).status); |
| |
| // No send (since H4 buffers are all in use). |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), kMaxSendCount - kAclBuffersSize); |
| // H4 buffers still all in use. |
| EXPECT_EQ(capture.in_flight_packets.size(), kAclBuffersSize); |
| |
| // This simulates the container/controller releasing an H4 buffer. That |
| // should result in another send to controller. |
| { |
| // We move the H4 packet out of the container so we can dtor it separately |
| // from the container's pop_back. The dtor of the H4 packet will trigger a |
| // push_back on the same container in this test's send_to_controller_fn |
| // lambda. We don't want that to happen nested inside a container pop_back |
| // as some containers (including pw::Vector and std::vector) don't handle |
| // nested modifications well. |
| H4PacketWithH4 last_packet = std::move(capture.in_flight_packets.back()); |
| } |
| // At this point the second to last in_flight_packets is the one we moved |
| // from. So erase that entry. |
| capture.in_flight_packets.erase( |
| std::prev(capture.in_flight_packets.end(), 2)); |
| |
| // Send of queued payload. |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize + 1); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), |
| kMaxSendCount - kAclBuffersSize - 1); |
| // We freed a H4 buffer, but then sending the queued payload used it. |
| EXPECT_EQ(capture.in_flight_packets.size(), kAclBuffersSize); |
| |
| // Free up remaining slots. |
| capture.in_flight_packets.clear(); |
| // There should have been no more sends since there were no payloads queued. |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize + 1); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), |
| kMaxSendCount - kAclBuffersSize - 1); |
| // And of course in flight packets are cleared (which indicates mean all H4 |
| // buffers are free for use). |
| EXPECT_EQ(capture.in_flight_packets.size(), 0u); |
| |
| // Confirm we can now reoccupy each H4 buffer slot. |
| for (size_t sent = 1; sent <= kAclBuffersSize; ++sent) { |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| // Each write is sent towards controller |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize + 1 + sent); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), |
| kMaxSendCount - kAclBuffersSize - 1 - sent); |
| // Container holds on to each H4 buffer. |
| EXPECT_EQ(capture.in_flight_packets.size(), sent); |
| } |
| |
| // If captured packets are not reset here, they may destruct after the proxy |
| // and lead to a crash when trying to lock the proxy's destructed mutex. |
| capture.in_flight_packets.clear(); |
| } |
| |
| TEST_F(MultiSendTest, CanRepeatedlyReuseOneBuffer) { |
| constexpr size_t kAclBuffersSize = |
| ProxyHost::GetNumSimultaneousAclSendsSupported(); |
| struct { |
| size_t sends_called = 0; |
| // These are packets that have been sent towards controller, but not |
| // released yet by container. |
| pw::Vector<H4PacketWithH4, kAclBuffersSize> in_flight_packets{}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&capture](H4PacketWithH4&& packet) { |
| ++capture.sends_called; |
| capture.in_flight_packets.push_back(std::move(packet)); |
| }); |
| |
| // Allow proxy to reserve enough credits for all the sends we do below. |
| // simultaneous sends supported by proxy. |
| constexpr size_t kTotalAclCredits = 2 * kAclBuffersSize; |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/kTotalAclCredits, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK( |
| SendLeReadBufferResponseFromController(proxy, kTotalAclCredits)); |
| |
| GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {}); |
| |
| std::array<uint8_t, 1> attribute_value = {0xF}; |
| |
| // Occupy all H4 buffers. |
| for (size_t sent = 1; sent <= kAclBuffersSize; ++sent) { |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| // Each write is sent towards controller |
| EXPECT_EQ(capture.sends_called, sent); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), kTotalAclCredits - sent); |
| // Container holds on to each H4 buffer. |
| EXPECT_EQ(capture.in_flight_packets.size(), sent); |
| } |
| |
| // This was already verified in last iteration of loop above, but we EXPECT |
| // explicitly here to provide reader context for EXPECTs in the loop below. |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), kTotalAclCredits - kAclBuffersSize); |
| EXPECT_EQ(capture.in_flight_packets.size(), kAclBuffersSize); |
| |
| // Repeatedly free and reoccupy last buffer. |
| for (size_t sent = 1; sent <= kAclBuffersSize; ++sent) { |
| capture.in_flight_packets.pop_back(); |
| // No send due to release of H4 buffer since no payloads were queued. |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize + sent - 1); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), |
| kTotalAclCredits - kAclBuffersSize - (sent - 1)); |
| // In flight packets has one free slot (which should align with one free H4 |
| // buffer slot). |
| EXPECT_EQ(capture.in_flight_packets.size(), kAclBuffersSize - 1); |
| |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| // Send happened using that one free H4 buffer slot. |
| EXPECT_EQ(capture.sends_called, kAclBuffersSize + sent); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), |
| kTotalAclCredits - kAclBuffersSize - sent); |
| // In flight packets full again, which should align with H4 |
| // buffers being full. |
| EXPECT_EQ(capture.in_flight_packets.size(), kAclBuffersSize); |
| } |
| |
| // If captured packets are not reset here, they may destruct after the proxy |
| // and lead to a crash when trying to lock the proxy's destructed mutex. |
| capture.in_flight_packets.clear(); |
| } |
| |
| TEST_F(MultiSendTest, CanSendOverManyDifferentConnections) { |
| std::array<uint8_t, 1> attribute_value = {0xF}; |
| struct { |
| uint16_t sends_called = 0; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [&capture]([[maybe_unused]] H4PacketWithH4&& packet) { |
| ++capture.sends_called; |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| ProxyHost::GetMaxNumAclConnections(), |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController( |
| proxy, ProxyHost::GetMaxNumAclConnections())); |
| |
| for (uint16_t send = 1; send <= ProxyHost::GetMaxNumAclConnections(); |
| send++) { |
| // Use current send count as the connection handle. |
| uint16_t conn_handle = send; |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = conn_handle}); |
| EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value)).status, |
| PW_STATUS_OK); |
| EXPECT_EQ(capture.sends_called, send); |
| } |
| } |
| |
| TEST_F(MultiSendTest, AttemptToCreateOverMaxConnectionsFails) { |
| constexpr uint16_t kSends = ProxyHost::GetMaxNumAclConnections() + 1; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/kSends, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, kSends)); |
| |
| std::vector<GattNotifyChannel> channels; |
| |
| for (uint16_t send = 1; send <= ProxyHost::GetMaxNumAclConnections(); |
| send++) { |
| // Use current send count as the connection handle. |
| uint16_t conn_handle = send; |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = conn_handle}); |
| channels.push_back(std::move(channel)); |
| } |
| |
| // Last one should fail |
| EXPECT_EQ( |
| BuildGattNotifyChannelWithResult(proxy, {.handle = kSends}).status(), |
| Status::Unavailable()); |
| } |
| |
| // ########## BasicL2capChannelTest |
| |
| class BasicL2capChannelTest : public ProxyHostTest {}; |
| |
| TEST_F(BasicL2capChannelTest, BasicWrite) { |
| struct { |
| int sends_called = 0; |
| // First four bits 0x0 encode PB & BC flags |
| uint16_t handle = 0x0ACB; |
| // Length of L2CAP PDU |
| uint16_t acl_data_total_length = 0x0007; |
| // L2CAP header PDU length field |
| uint16_t pdu_length = 0x0003; |
| // Random CID |
| uint16_t channel_id = 0x1234; |
| // L2CAP information payload |
| std::array<uint8_t, 3> payload = {0xAB, 0xCD, 0xEF}; |
| |
| // Built from the preceding values in little endian order (except payload in |
| // big endian). |
| std::array<uint8_t, 11> expected_hci_packet = { |
| 0xCB, 0x0A, 0x07, 0x00, 0x03, 0x00, 0x34, 0x12, 0xAB, 0xCD, 0xEF}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [&capture](H4PacketWithH4&& packet) { |
| ++capture.sends_called; |
| EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::ACL_DATA); |
| EXPECT_EQ(packet.GetHciSpan().size(), |
| capture.expected_hci_packet.size()); |
| EXPECT_TRUE(std::equal(packet.GetHciSpan().begin(), |
| packet.GetHciSpan().end(), |
| capture.expected_hci_packet.begin(), |
| capture.expected_hci_packet.end())); |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto acl, |
| MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan())); |
| EXPECT_EQ(acl.header().handle().Read(), capture.handle); |
| EXPECT_EQ(acl.header().packet_boundary_flag().Read(), |
| emboss::AclDataPacketBoundaryFlag::FIRST_NON_FLUSHABLE); |
| EXPECT_EQ(acl.header().broadcast_flag().Read(), |
| emboss::AclDataPacketBroadcastFlag::POINT_TO_POINT); |
| EXPECT_EQ(acl.data_total_length().Read(), |
| capture.acl_data_total_length); |
| emboss::BFrameView bframe = emboss::MakeBFrameView( |
| acl.payload().BackingStorage().data(), acl.SizeInBytes()); |
| EXPECT_EQ(bframe.pdu_length().Read(), capture.pdu_length); |
| EXPECT_EQ(bframe.channel_id().Read(), capture.channel_id); |
| for (size_t i = 0; i < 3; ++i) { |
| EXPECT_EQ(bframe.payload()[i].Read(), capture.payload[i]); |
| } |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| // Allow proxy to reserve 1 LE credit. |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| |
| BasicL2capChannel channel = |
| BuildBasicL2capChannel(proxy, |
| {.handle = capture.handle, |
| .local_cid = 0x123, |
| .remote_cid = capture.channel_id, |
| .transport = AclTransportType::kLe}); |
| |
| PW_TEST_EXPECT_OK( |
| channel.Write(MultiBufFromSpan(pw::span(capture.payload))).status); |
| EXPECT_EQ(capture.sends_called, 1); |
| } |
| |
| TEST_F(BasicL2capChannelTest, ErrorOnWriteTooLarge) { |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [](H4PacketWithH4&&) { FAIL(); }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| // Allow proxy to reserve 1 credit. |
| PW_TEST_EXPECT_OK(SendReadBufferResponseFromController(proxy, 1)); |
| |
| std::array<uint8_t, |
| ProxyHost::GetMaxAclSendSize() - |
| emboss::AclDataFrameHeader::IntrinsicSizeInBytes() - |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + 1> |
| hci_arr; |
| |
| BasicL2capChannel channel = |
| BuildBasicL2capChannel(proxy, |
| {.handle = 0x123, |
| .local_cid = 0x123, |
| .remote_cid = 0x123, |
| .transport = AclTransportType::kLe}); |
| |
| EXPECT_EQ(channel.Write(MultiBufFromSpan(pw::span(hci_arr))).status, |
| PW_STATUS_INVALID_ARGUMENT); |
| } |
| |
| TEST_F(BasicL2capChannelTest, CannotCreateChannelWithInvalidArgs) { |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| // Connection handle too large by 1. |
| |
| Result<BasicL2capChannel> channel = |
| BuildBasicL2capChannelWithResult(proxy, |
| {.handle = 0x0FFF, |
| .local_cid = 0x123, |
| .remote_cid = 0x123, |
| .transport = AclTransportType::kLe}); |
| EXPECT_EQ(channel.status(), Status::InvalidArgument()); |
| |
| // Local CID invalid (0). |
| channel = |
| BuildBasicL2capChannelWithResult(proxy, |
| BasicL2capParameters{ |
| .handle = 0x123, |
| .local_cid = 0, |
| .remote_cid = 0x123, |
| .transport = AclTransportType::kLe, |
| }); |
| EXPECT_EQ(channel.status(), Status::InvalidArgument()); |
| } |
| |
| TEST_F(BasicL2capChannelTest, BasicRead) { |
| struct { |
| int sends_called = 0; |
| int to_host_called = 0; |
| std::array<uint8_t, 3> expected_payload = {0xAB, 0xCD, 0xEF}; |
| } capture; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [&capture](H4PacketWithHci&&) { ++capture.to_host_called; }); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| uint16_t handle = 334; |
| uint16_t local_cid = 443; |
| BasicL2capChannel channel = BuildBasicL2capChannel( |
| proxy, |
| BasicL2capParameters{ |
| .handle = handle, |
| .local_cid = local_cid, |
| .remote_cid = 0x123, |
| .transport = AclTransportType::kLe, |
| .payload_from_controller_fn = |
| [&capture](multibuf::MultiBuf&& buffer) { |
| ++capture.sends_called; |
| std::optional<pw::ByteSpan> payload = buffer.ContiguousSpan(); |
| ConstByteSpan expected_bytes = |
| as_bytes(span(capture.expected_payload.data(), |
| capture.expected_payload.size())); |
| EXPECT_TRUE(payload.has_value()); |
| EXPECT_TRUE(std::equal(payload->begin(), |
| payload->end(), |
| expected_bytes.begin(), |
| expected_bytes.end())); |
| return std::nullopt; |
| }, |
| }); |
| |
| std::array<uint8_t, |
| emboss::AclDataFrameHeader::IntrinsicSizeInBytes() + |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| capture.expected_payload.size()> |
| hci_arr; |
| hci_arr.fill(0); |
| H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr}; |
| |
| Result<emboss::AclDataFrameWriter> acl = |
| MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr); |
| acl->header().handle().Write(handle); |
| acl->data_total_length().Write( |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| capture.expected_payload.size()); |
| |
| emboss::BFrameWriter bframe = emboss::MakeBFrameView( |
| acl->payload().BackingStorage().data(), acl->payload().SizeInBytes()); |
| bframe.pdu_length().Write(capture.expected_payload.size()); |
| bframe.channel_id().Write(local_cid); |
| std::copy(capture.expected_payload.begin(), |
| capture.expected_payload.end(), |
| hci_arr.begin() + |
| emboss::AclDataFrameHeader::IntrinsicSizeInBytes() + |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes()); |
| |
| // Send ACL data packet destined for the CoC we registered. |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(capture.sends_called, 1); |
| EXPECT_EQ(capture.to_host_called, 0); |
| } |
| |
| TEST_F(BasicL2capChannelTest, BasicForward) { |
| struct { |
| int sends_called = 0; |
| int to_host_called = 0; |
| std::array<uint8_t, 3> expected_payload = {0xAB, 0xCD, 0xEF}; |
| std::array<uint8_t, |
| emboss::AclDataFrameHeader::IntrinsicSizeInBytes() + |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + 3> |
| hci_arr{}; |
| } capture; |
| |
| H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, capture.hci_arr}; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [&capture](H4PacketWithHci&& packet) { |
| ++capture.to_host_called; |
| EXPECT_TRUE(std::equal(packet.GetHciSpan().begin(), |
| packet.GetHciSpan().end(), |
| capture.hci_arr.begin(), |
| capture.hci_arr.end())); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| uint16_t handle = 334; |
| uint16_t local_cid = 443; |
| BasicL2capChannel channel = |
| BuildBasicL2capChannel(proxy, |
| BasicL2capParameters{ |
| .handle = handle, |
| .local_cid = local_cid, |
| .remote_cid = 0x123, |
| .transport = AclTransportType::kLe, |
| .payload_from_controller_fn = |
| [&capture](multibuf::MultiBuf&& buffer) { |
| ++capture.sends_called; |
| // Forward to host. |
| return std::move(buffer); |
| }, |
| }); |
| |
| Result<emboss::AclDataFrameWriter> acl = |
| MakeEmbossWriter<emboss::AclDataFrameWriter>(capture.hci_arr); |
| acl->header().handle().Write(handle); |
| acl->data_total_length().Write( |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| capture.expected_payload.size()); |
| |
| emboss::BFrameWriter bframe = emboss::MakeBFrameView( |
| acl->payload().BackingStorage().data(), acl->payload().SizeInBytes()); |
| bframe.pdu_length().Write(capture.expected_payload.size()); |
| bframe.channel_id().Write(local_cid); |
| std::copy(capture.expected_payload.begin(), |
| capture.expected_payload.end(), |
| capture.hci_arr.begin() + |
| emboss::AclDataFrameHeader::IntrinsicSizeInBytes() + |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes()); |
| |
| // Send ACL data packet destined for the CoC we registered. |
| proxy.HandleH4HciFromController(std::move(h4_packet)); |
| |
| EXPECT_EQ(capture.sends_called, 1); |
| EXPECT_EQ(capture.to_host_called, 1); |
| } |
| |
| TEST_F(BasicL2capChannelTest, ReadPacketToController) { |
| struct { |
| int sends_called = 0; |
| int from_host_called = 0; |
| std::array<uint8_t, 3> expected_payload = {0xAB, 0xCD, 0xEF}; |
| std::array<uint8_t, |
| emboss::AclDataFrameHeader::IntrinsicSizeInBytes() + |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + 3> |
| hci_arr{}; |
| } capture; |
| |
| std::array<uint8_t, sizeof(emboss::H4PacketType) + capture.hci_arr.size()> |
| h4_arr; |
| h4_arr[0] = cpp23::to_underlying(emboss::H4PacketType::ACL_DATA); |
| H4PacketWithH4 h4_packet{h4_arr}; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [&capture](H4PacketWithH4&& packet) { |
| ++capture.from_host_called; |
| EXPECT_TRUE(std::equal(packet.GetHciSpan().begin(), |
| packet.GetHciSpan().end(), |
| capture.hci_arr.begin(), |
| capture.hci_arr.end())); |
| }); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| uint16_t handle = 0x334; |
| uint16_t local_cid = 0x443; |
| uint16_t remote_cid = 0x123; |
| BasicL2capChannel channel = |
| BuildBasicL2capChannel(proxy, |
| BasicL2capParameters{ |
| .handle = handle, |
| .local_cid = local_cid, |
| .remote_cid = remote_cid, |
| .transport = AclTransportType::kBrEdr, |
| .payload_from_host_fn = |
| [&capture](multibuf::MultiBuf&& buffer) { |
| ++capture.sends_called; |
| return std::move(buffer); |
| }, |
| }); |
| |
| Result<emboss::AclDataFrameWriter> acl = |
| MakeEmbossWriter<emboss::AclDataFrameWriter>(capture.hci_arr); |
| acl->header().handle().Write(handle); |
| acl->data_total_length().Write( |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| capture.expected_payload.size()); |
| |
| emboss::BasicL2capHeaderWriter l2cap_header = |
| emboss::MakeBasicL2capHeaderView( |
| acl->payload().BackingStorage().data(), |
| acl->payload().BackingStorage().SizeInBytes()); |
| l2cap_header.pdu_length().Write(capture.expected_payload.size()); |
| l2cap_header.channel_id().Write(remote_cid); |
| |
| std::copy(capture.expected_payload.begin(), |
| capture.expected_payload.end(), |
| capture.hci_arr.begin() + |
| emboss::AclDataFrameHeader::IntrinsicSizeInBytes() + |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes()); |
| |
| std::copy(capture.hci_arr.begin(), capture.hci_arr.end(), h4_arr.begin() + 1); |
| |
| proxy.HandleH4HciFromHost(std::move(h4_packet)); |
| |
| EXPECT_EQ(capture.from_host_called, 1); |
| EXPECT_EQ(capture.sends_called, 1); |
| } |
| |
| // ########## L2capSignalingTest |
| |
| class L2capSignalingTest : public ProxyHostTest {}; |
| |
| TEST_F(L2capSignalingTest, FlowControlCreditIndDrainsQueue) { |
| size_t sends_called = 0; |
| |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [&sends_called]([[maybe_unused]] H4PacketWithH4&& packet) { |
| ++sends_called; |
| }); |
| ProxyHost proxy = |
| ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/L2capCoc::QueueCapacity(), |
| /*br_edr_acl_credits_to_reserve=*/0); |
| PW_TEST_EXPECT_OK( |
| SendLeReadBufferResponseFromController(proxy, L2capCoc::QueueCapacity())); |
| EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), L2capCoc::QueueCapacity()); |
| |
| uint16_t handle = 123; |
| uint16_t remote_cid = 456; |
| L2capCoc channel = BuildCoc( |
| proxy, |
| CocParameters{ |
| .handle = handle, .remote_cid = remote_cid, .tx_credits = 0}); |
| |
| for (size_t i = 0; i < L2capCoc::QueueCapacity(); ++i) { |
| PW_TEST_EXPECT_OK(channel.Write(multibuf::MultiBuf{}).status); |
| } |
| EXPECT_EQ(channel.Write(multibuf::MultiBuf{}).status, Status::Unavailable()); |
| EXPECT_EQ(sends_called, 0u); |
| |
| constexpr size_t kL2capLength = |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes(); |
| constexpr size_t kHciLength = |
| emboss::AclDataFrame::MinSizeInBytes() + kL2capLength; |
| std::array<uint8_t, kHciLength> hci_arr; |
| hci_arr.fill(0); |
| H4PacketWithHci flow_control_credit_ind{emboss::H4PacketType::ACL_DATA, |
| pw::span(hci_arr.data(), kHciLength)}; |
| |
| Result<emboss::AclDataFrameWriter> acl = |
| MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr); |
| acl->header().handle().Write(handle); |
| acl->data_total_length().Write(kL2capLength); |
| |
| emboss::CFrameWriter l2cap = emboss::MakeCFrameView( |
| acl->payload().BackingStorage().data(), kL2capLength); |
| l2cap.pdu_length().Write( |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| // 0x0005 = LE-U fixed signaling channel ID. |
| l2cap.channel_id().Write(0x0005); |
| |
| emboss::L2capFlowControlCreditIndWriter ind = |
| emboss::MakeL2capFlowControlCreditIndView( |
| l2cap.payload().BackingStorage().data(), |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| ind.command_header().code().Write( |
| emboss::L2capSignalingPacketCode::FLOW_CONTROL_CREDIT_IND); |
| ind.command_header().data_length().Write( |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes() - |
| emboss::L2capSignalingCommandHeader::IntrinsicSizeInBytes()); |
| ind.cid().Write(remote_cid); |
| ind.credits().Write(L2capCoc::QueueCapacity()); |
| |
| proxy.HandleH4HciFromController(std::move(flow_control_credit_ind)); |
| |
| EXPECT_EQ(sends_called, L2capCoc::QueueCapacity()); |
| } |
| |
| TEST_F(L2capSignalingTest, ChannelClosedWithErrorIfCreditsExceeded) { |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = |
| ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/L2capCoc::QueueCapacity(), |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| uint16_t handle = 123; |
| uint16_t remote_cid = 456; |
| int events_received = 0; |
| L2capCoc channel = BuildCoc( |
| proxy, |
| CocParameters{ |
| .handle = handle, |
| .remote_cid = remote_cid, |
| // Initialize with max credit count. |
| .tx_credits = |
| emboss::L2capLeCreditBasedConnectionReq::max_credit_value(), |
| .event_fn = [&events_received](L2capChannelEvent event) { |
| EXPECT_EQ(event, L2capChannelEvent::kRxInvalid); |
| ++events_received; |
| }}); |
| |
| constexpr size_t kL2capLength = |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes(); |
| constexpr size_t kHciLength = |
| emboss::AclDataFrame::MinSizeInBytes() + kL2capLength; |
| std::array<uint8_t, kHciLength> hci_arr; |
| hci_arr.fill(0); |
| H4PacketWithHci flow_control_credit_ind{emboss::H4PacketType::ACL_DATA, |
| pw::span(hci_arr.data(), kHciLength)}; |
| |
| Result<emboss::AclDataFrameWriter> acl = |
| MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr); |
| acl->header().handle().Write(handle); |
| acl->data_total_length().Write(kL2capLength); |
| |
| emboss::CFrameWriter l2cap = |
| emboss::MakeCFrameView(acl->payload().BackingStorage().data(), |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes()); |
| l2cap.pdu_length().Write( |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| // 0x0005 = LE-U fixed signaling channel ID. |
| l2cap.channel_id().Write(0x0005); |
| |
| emboss::L2capFlowControlCreditIndWriter ind = |
| emboss::MakeL2capFlowControlCreditIndView( |
| l2cap.payload().BackingStorage().data(), |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| ind.command_header().code().Write( |
| emboss::L2capSignalingPacketCode::FLOW_CONTROL_CREDIT_IND); |
| ind.command_header().data_length().Write( |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes() - |
| emboss::L2capSignalingCommandHeader::IntrinsicSizeInBytes()); |
| ind.cid().Write(remote_cid); |
| // Exceed max credit count by 1. |
| ind.credits().Write(1); |
| |
| proxy.HandleH4HciFromController(std::move(flow_control_credit_ind)); |
| |
| EXPECT_EQ(events_received, 1); |
| } |
| |
| TEST_F(L2capSignalingTest, SignalsArePassedOnToHost) { |
| int forwards_to_host = 0; |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [&forwards_to_host](H4PacketWithHci&&) { ++forwards_to_host; }); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| EXPECT_EQ(forwards_to_host, 0); |
| |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionReq(proxy, Direction::kFromController, 44, 55, 56)); |
| EXPECT_EQ(forwards_to_host, 1); |
| } |
| |
| TEST_F(L2capSignalingTest, SignalsArePassedOnToHostAfterAclDisconnect) { |
| uint16_t kConnHandle = 0x33; |
| int sends_to_host = 0; |
| int sends_to_controller = 0; |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [&sends_to_host](H4PacketWithHci&&) { ++sends_to_host; }); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [&sends_to_controller](H4PacketWithH4&&) { ++sends_to_controller; }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| // Allow proxy to reserve 1 credit. |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| EXPECT_EQ(sends_to_host, 1); |
| |
| // Send GATT Notify which should create ACL connection for kConnHandle. |
| std::array<uint8_t, 1> attribute_value = {0}; |
| { |
| GattNotifyChannel channel = |
| BuildGattNotifyChannel(proxy, {.handle = kConnHandle}); |
| PW_TEST_EXPECT_OK(channel.Write(MultiBufFromArray(attribute_value)).status); |
| } |
| EXPECT_EQ(sends_to_controller, 1); |
| |
| // Disconnect that connection. |
| PW_TEST_EXPECT_OK( |
| SendDisconnectionCompleteEvent(proxy, /*handle=*/kConnHandle)); |
| EXPECT_EQ(sends_to_host, 2); |
| |
| // Send signal again using the same connection. Signal should be passed on |
| // to host. |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromController, /*handle=*/kConnHandle, 55, 56)); |
| EXPECT_EQ(sends_to_host, 3); |
| |
| // Trigger credit send for L2capCoc to verify new signalling channel |
| // object is present and working. |
| { |
| L2capCoc channel = BuildCoc(proxy, CocParameters{.handle = kConnHandle}); |
| PW_TEST_EXPECT_OK(channel.SendAdditionalRxCredits(7)); |
| } |
| EXPECT_EQ(sends_to_controller, 2); |
| } |
| |
| TEST_F(L2capSignalingTest, |
| CreditIndAddressedToNonManagedChannelForwardedToHost) { |
| int forwards_to_host = 0; |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [&forwards_to_host](H4PacketWithHci&&) { ++forwards_to_host; }); |
| pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| |
| ProxyHost proxy = |
| ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/L2capCoc::QueueCapacity(), |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| uint16_t handle = 123; |
| uint16_t remote_cid = 456; |
| L2capCoc channel = BuildCoc( |
| proxy, CocParameters{.handle = handle, .remote_cid = remote_cid}); |
| |
| constexpr size_t kL2capLength = |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes(); |
| constexpr size_t kHciLength = |
| emboss::AclDataFrame::MinSizeInBytes() + kL2capLength; |
| std::array<uint8_t, kHciLength> hci_arr; |
| hci_arr.fill(0); |
| H4PacketWithHci flow_control_credit_ind{emboss::H4PacketType::ACL_DATA, |
| pw::span(hci_arr.data(), kHciLength)}; |
| |
| Result<emboss::AclDataFrameWriter> acl = |
| MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr); |
| acl->header().handle().Write(handle); |
| acl->data_total_length().Write(kL2capLength); |
| |
| emboss::CFrameWriter l2cap = |
| emboss::MakeCFrameView(acl->payload().BackingStorage().data(), |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes()); |
| l2cap.pdu_length().Write( |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| // 0x0005 = LE-U fixed signaling channel ID. |
| l2cap.channel_id().Write(0x0005); |
| |
| emboss::L2capFlowControlCreditIndWriter ind = |
| emboss::MakeL2capFlowControlCreditIndView( |
| l2cap.payload().BackingStorage().data(), |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| ind.command_header().code().Write( |
| emboss::L2capSignalingPacketCode::FLOW_CONTROL_CREDIT_IND); |
| ind.command_header().data_length().Write( |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes() - |
| emboss::L2capSignalingCommandHeader::IntrinsicSizeInBytes()); |
| // Address packet to different CID on same connection. |
| ind.cid().Write(remote_cid + 1); |
| |
| proxy.HandleH4HciFromController(std::move(flow_control_credit_ind)); |
| |
| EXPECT_EQ(forwards_to_host, 1); |
| } |
| |
| TEST_F(L2capSignalingTest, RxAdditionalCreditsSent) { |
| struct { |
| uint16_t handle = 123; |
| uint16_t local_cid = 456; |
| uint16_t credits = 3; |
| 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&& packet) { |
| ++capture.sends_called; |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto acl, |
| MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan())); |
| EXPECT_EQ(acl.header().handle().Read(), capture.handle); |
| EXPECT_EQ( |
| acl.data_total_length().Read(), |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| emboss::CFrameView cframe = emboss::MakeCFrameView( |
| acl.payload().BackingStorage().data(), acl.payload().SizeInBytes()); |
| EXPECT_EQ(cframe.pdu_length().Read(), |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes()); |
| // 0x0005 = LE-U fixed signaling channel ID. |
| EXPECT_EQ(cframe.channel_id().Read(), 0x0005); |
| emboss::L2capFlowControlCreditIndView ind = |
| emboss::MakeL2capFlowControlCreditIndView( |
| cframe.payload().BackingStorage().data(), |
| cframe.payload().SizeInBytes()); |
| EXPECT_EQ(ind.command_header().code().Read(), |
| emboss::L2capSignalingPacketCode::FLOW_CONTROL_CREDIT_IND); |
| // TODO: https://pwbug.dev/382553099 - Test to ensure we are properly |
| // incrementing Identifier when sending multiple signaling packets. |
| EXPECT_EQ(ind.command_header().identifier().Read(), 1); |
| EXPECT_EQ( |
| ind.command_header().data_length().Read(), |
| emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes() - |
| emboss::L2capSignalingCommandHeader::IntrinsicSizeInBytes()); |
| EXPECT_EQ(ind.cid().Read(), capture.local_cid); |
| EXPECT_EQ(ind.credits().Read(), capture.credits); |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| // Allow proxy to reserve 1 LE credit. |
| PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1)); |
| |
| // Build channel so ACL connection is registered. |
| L2capCoc channel = BuildCoc( |
| proxy, |
| CocParameters{.handle = capture.handle, .local_cid = capture.local_cid}); |
| |
| PW_TEST_EXPECT_OK(channel.SendAdditionalRxCredits(capture.credits)); |
| |
| EXPECT_EQ(capture.sends_called, 1); |
| } |
| |
| TEST_F(L2capSignalingTest, RemoteLocalCidCollisionBetweenProfiles) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/5); |
| |
| // We are receiving the connection and disconnection request for two different |
| // channels where one channel has same local CID and the others remote CID. |
| constexpr uint16_t kHandle = 0x123; |
| |
| constexpr uint8_t kPsm = 0x19; |
| constexpr uint16_t kRemoteCID = 0x65; |
| constexpr uint16_t kLocalCID = 0x46; |
| |
| constexpr uint8_t kPsm2 = 0x1B; |
| constexpr uint16_t kRemoteCID2 = 0x46; |
| constexpr uint16_t kLocalCID2 = 0x4f; |
| |
| // Receive L2capConnectionReq on first PSM |
| EXPECT_EQ(SendL2capConnectionReq( |
| proxy, Direction::kFromController, kHandle, kRemoteCID, kPsm), |
| pw::OkStatus()); |
| EXPECT_EQ( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kRemoteCID, |
| kLocalCID, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL), |
| pw::OkStatus()); |
| |
| // Acquire first channel with the event_fn_ |
| uint8_t reset_called = 0; |
| pw::multibuf::test::SimpleAllocatorForTest</*kDataSizeBytes=*/1024, |
| /*kMetaSizeBytes=*/256> |
| multibuf_allocator_{}; |
| |
| auto event_fn([&reset_called](L2capChannelEvent event) -> void { |
| switch (event) { |
| case L2capChannelEvent::kChannelClosedByOther: |
| case L2capChannelEvent::kReset: |
| reset_called++; |
| break; |
| case L2capChannelEvent::kRxInvalid: |
| case L2capChannelEvent::kRxOutOfMemory: |
| case L2capChannelEvent::kRxWhileStopped: |
| case L2capChannelEvent::kWriteAvailable: |
| default: |
| break; |
| } |
| }); |
| BasicL2capChannel channel = BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kLocalCID, |
| .remote_cid = kRemoteCID, |
| .event_fn = event_fn}); |
| |
| // Receive L2capConnectionReq on second PSM |
| EXPECT_EQ(SendL2capConnectionReq( |
| proxy, Direction::kFromController, kHandle, kRemoteCID2, kPsm2), |
| pw::OkStatus()); |
| EXPECT_EQ( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kRemoteCID2, |
| kLocalCID2, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL), |
| pw::OkStatus()); |
| |
| // Send Disconnect second channel (Received this disconnect rsp) |
| EXPECT_EQ(SendL2capDisconnectRsp(proxy, |
| Direction::kFromController, |
| AclTransportType::kBrEdr, |
| kHandle, |
| kLocalCID2, |
| kRemoteCID2), |
| pw::OkStatus()); |
| |
| // Assert first event channel wasn't called |
| EXPECT_EQ(reset_called, 0); |
| |
| // Send Disconnect first channel (Received this disconnect rsp) |
| EXPECT_EQ(SendL2capDisconnectRsp(proxy, |
| Direction::kFromController, |
| AclTransportType::kBrEdr, |
| kHandle, |
| kLocalCID, |
| kRemoteCID), |
| pw::OkStatus()); |
| |
| // Assert first event channel was called |
| EXPECT_EQ(reset_called, 1); |
| } |
| |
| // ########## AcluSignalingChannelTest |
| |
| class AcluSignalingChannelTest : public ProxyHostTest {}; |
| |
| TEST_F(AcluSignalingChannelTest, HandlesMultipleCommands) { |
| std::optional<H4PacketWithHci> host_packet; |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [&host_packet](H4PacketWithHci&& packet) { |
| host_packet = std::move(packet); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| constexpr uint16_t kHandle = 123; |
| |
| // Test that the proxy can parse a CFrame containing multiple commands and |
| // pass it through. We pack 3 CONNECTION_REQ commands into one CFrame. |
| constexpr size_t kNumCommands = 3; |
| constexpr size_t kCmdLen = emboss::L2capConnectionReq::IntrinsicSizeInBytes(); |
| constexpr size_t kL2capLength = |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + kCmdLen * kNumCommands; |
| constexpr size_t kHciLength = |
| emboss::AclDataFrame::MinSizeInBytes() + kL2capLength; |
| std::array<uint8_t, kHciLength> hci_arr{}; |
| H4PacketWithHci l2cap_cframe_packet{emboss::H4PacketType::ACL_DATA, |
| pw::span(hci_arr.data(), kHciLength)}; |
| |
| // ACL header |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto acl, MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr)); |
| acl.header().handle().Write(kHandle); |
| acl.data_total_length().Write(kL2capLength); |
| EXPECT_EQ(kL2capLength, acl.payload().BackingStorage().SizeInBytes()); |
| |
| // L2CAP header |
| auto l2cap = |
| emboss::MakeCFrameView(acl.payload().BackingStorage().data(), |
| acl.payload().BackingStorage().SizeInBytes()); |
| l2cap.pdu_length().Write(kNumCommands * kCmdLen); |
| l2cap.channel_id().Write( |
| cpp23::to_underlying(emboss::L2capFixedCid::ACL_U_SIGNALING)); |
| EXPECT_TRUE(l2cap.Ok()); |
| |
| auto command_buffer = |
| pw::span(l2cap.payload().BackingStorage().data(), |
| l2cap.payload().BackingStorage().SizeInBytes()); |
| EXPECT_EQ(l2cap.payload().BackingStorage().SizeInBytes(), |
| kCmdLen * kNumCommands); |
| |
| do { |
| // CONNECTION_REQ |
| auto cmd_writer = emboss::MakeL2capConnectionReqView(command_buffer.data(), |
| command_buffer.size()); |
| cmd_writer.command_header().code().Write( |
| emboss::L2capSignalingPacketCode::CONNECTION_REQ); |
| // Note data_length doesn't include command header. |
| cmd_writer.command_header().data_length().Write( |
| kCmdLen - emboss::L2capSignalingCommandHeader::IntrinsicSizeInBytes()); |
| cmd_writer.psm().Write(1); |
| cmd_writer.source_cid().Write(1); |
| EXPECT_TRUE(cmd_writer.Ok()); |
| EXPECT_EQ(cmd_writer.SizeInBytes(), kCmdLen); |
| command_buffer = command_buffer.subspan(cmd_writer.SizeInBytes()); |
| } while (!command_buffer.empty()); |
| |
| proxy.HandleH4HciFromController(std::move(l2cap_cframe_packet)); |
| // We should get back what we sent, since the proxy doesn't consume |
| // CONNECTION_REQ commands. It would be nice to also verify the individual |
| // commands were parsed out but hooks don't exist for that at the time of |
| // writing. |
| EXPECT_TRUE(host_packet.has_value()); |
| EXPECT_EQ(host_packet->GetHciSpan().size(), kHciLength); |
| } |
| |
| TEST_F(AcluSignalingChannelTest, InvalidPacketForwarded) { |
| std::optional<H4PacketWithHci> host_packet; |
| pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn( |
| [&host_packet](H4PacketWithHci&& packet) { |
| host_packet = std::move(packet); |
| }); |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/1, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| constexpr uint16_t kHandle = 123; |
| |
| // Test that the proxy forwards on invalid L2cap B-frames destined for |
| // signaling channel. |
| |
| constexpr size_t kL2capLength = |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes(); |
| constexpr size_t kHciLength = |
| emboss::AclDataFrame::MinSizeInBytes() + kL2capLength; |
| std::array<uint8_t, kHciLength> hci_arr{}; |
| H4PacketWithHci l2cap_cframe_packet{emboss::H4PacketType::ACL_DATA, |
| pw::span(hci_arr.data(), kHciLength)}; |
| |
| // ACL header |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto acl, MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr)); |
| acl.header().handle().Write(kHandle); |
| acl.data_total_length().Write(kL2capLength); |
| EXPECT_EQ(kL2capLength, acl.payload().BackingStorage().SizeInBytes()); |
| |
| // L2CAP header |
| auto l2cap = |
| emboss::MakeCFrameView(acl.payload().BackingStorage().data(), |
| acl.payload().BackingStorage().SizeInBytes()); |
| // Invalid length, since we aren't encoding a payload. |
| l2cap.pdu_length().Write(1); |
| l2cap.channel_id().Write( |
| cpp23::to_underlying(emboss::L2capFixedCid::ACL_U_SIGNALING)); |
| EXPECT_FALSE(l2cap.Ok()); |
| |
| proxy.HandleH4HciFromController(std::move(l2cap_cframe_packet)); |
| // We should get back what we sent. |
| EXPECT_TRUE(host_packet.has_value()); |
| EXPECT_EQ(host_packet->GetHciSpan().size(), kHciLength); |
| } |
| |
| // ########## ProxyHostConnectionEventTest |
| |
| class ProxyHostConnectionEventTest : public ProxyHostTest {}; |
| |
| TEST_F(ProxyHostConnectionEventTest, ConnectionCompletePassthroughOk) { |
| size_t host_called = 0; |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&host_called]([[maybe_unused]] H4PacketWithHci&& packet) { |
| ++host_called; |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| PW_TEST_EXPECT_OK( |
| SendConnectionCompleteEvent(proxy, 1, emboss::StatusCode::SUCCESS)); |
| EXPECT_EQ(host_called, 1U); |
| |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, 1)); |
| EXPECT_EQ(host_called, 2U); |
| } |
| |
| TEST_F(ProxyHostConnectionEventTest, |
| ConnectionCompleteWithErrorStatusPassthroughOk) { |
| size_t host_called = 0; |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&host_called]([[maybe_unused]] H4PacketWithHci&& packet) { |
| ++host_called; |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| PW_TEST_EXPECT_OK(SendConnectionCompleteEvent( |
| proxy, 1, emboss::StatusCode::CONNECTION_FAILED_TO_BE_ESTABLISHED)); |
| EXPECT_EQ(host_called, 1U); |
| } |
| |
| TEST_F(ProxyHostConnectionEventTest, LeConnectionCompletePassthroughOk) { |
| size_t host_called = 0; |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [&host_called]([[maybe_unused]] H4PacketWithHci&& packet) { |
| ++host_called; |
| }); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| PW_TEST_EXPECT_OK( |
| SendLeConnectionCompleteEvent(proxy, 1, emboss::StatusCode::SUCCESS)); |
| EXPECT_EQ(host_called, 1U); |
| } |
| |
| class L2capStatusTrackerTest : public ProxyHostTest, |
| public L2capStatusDelegate { |
| public: |
| static constexpr uint16_t kPsm = 1; |
| |
| bool ShouldTrackPsm(uint16_t psm) override { return psm == kPsm; } |
| void HandleConnectionComplete(const L2capChannelConnectionInfo& i) override { |
| EXPECT_FALSE(info.has_value()); |
| PW_CHECK(proxy_ptr); |
| info.emplace(i); |
| // Test we can create channel directly in callback. |
| l2cap_channel = |
| BuildBasicL2capChannel(*proxy_ptr, |
| {.handle = i.connection_handle, |
| .local_cid = i.local_cid, |
| .remote_cid = i.remote_cid, |
| .transport = AclTransportType::kBrEdr}); |
| } |
| void HandleDisconnectionComplete( |
| const L2capChannelConnectionInfo& i) override { |
| ASSERT_TRUE(info.has_value()); |
| EXPECT_EQ(info->direction, i.direction); |
| EXPECT_EQ(info->connection_handle, i.connection_handle); |
| EXPECT_EQ(info->remote_cid, i.remote_cid); |
| EXPECT_EQ(info->local_cid, i.local_cid); |
| info.reset(); |
| } |
| |
| void HandleConfigurationChanged( |
| const L2capChannelConfigurationInfo& i) override { |
| configuration_called++; |
| PW_CHECK(proxy_ptr); |
| |
| EXPECT_EQ(config_info->direction, i.direction); |
| EXPECT_EQ(config_info->connection_handle, i.connection_handle); |
| EXPECT_EQ(config_info->local_cid, i.local_cid); |
| EXPECT_EQ(config_info->mtu, i.mtu); |
| } |
| |
| ProxyHost* proxy_ptr = nullptr; |
| uint8_t configuration_called = 0; |
| std::optional<L2capChannelConnectionInfo> info; |
| std::optional<BasicL2capChannel> l2cap_channel; |
| std::optional<L2capChannelConfigurationInfo> config_info; |
| }; |
| |
| // TODO(b/405201804): Add test that check MTU value in the response |
| TEST_F(L2capStatusTrackerTest, L2capConfigurationMTUCalled) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy_ptr = &proxy; |
| |
| constexpr uint16_t kLocalCid = 30; |
| constexpr uint16_t kRemoteCid = 31; |
| constexpr uint16_t kHandle = 123; |
| |
| proxy.RegisterL2capStatusDelegate(*this); |
| |
| PW_TEST_EXPECT_OK( |
| SendConnectionCompleteEvent(proxy, kHandle, emboss::StatusCode::SUCCESS)); |
| |
| // Receive new connection req |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromController, kHandle, kRemoteCid, kPsm)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send success rsp |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kRemoteCid, |
| kLocalCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| |
| auto l2cap_options = L2capOptions{ |
| .mtu = MtuOption{1024}, |
| }; |
| |
| // Send Configure Request |
| auto expected_sent_l2cap_configuration = L2capChannelConfigurationInfo{ |
| .direction = Direction::kFromHost, |
| .connection_handle = kHandle, |
| .remote_cid = kRemoteCid, |
| .local_cid = kLocalCid, |
| .mtu = MtuOption{1024}, |
| }; |
| config_info.emplace(expected_sent_l2cap_configuration); |
| |
| PW_TEST_EXPECT_OK(SendL2capConfigureReq( |
| proxy, Direction::kFromHost, kHandle, kRemoteCid, l2cap_options)); |
| |
| PW_TEST_EXPECT_OK( |
| SendL2capConfigureRsp(proxy, |
| Direction::kFromController, |
| kHandle, |
| kLocalCid, |
| emboss::L2capConfigurationResult::SUCCESS)); |
| ASSERT_EQ(this->configuration_called, 1); |
| |
| // Receive Configure Request |
| auto expected_recv_l2cap_configuration = L2capChannelConfigurationInfo{ |
| .direction = Direction::kFromController, |
| .connection_handle = kHandle, |
| .remote_cid = kRemoteCid, |
| .local_cid = kLocalCid, |
| .mtu = MtuOption{1024}, |
| }; |
| |
| config_info.emplace(expected_recv_l2cap_configuration); |
| |
| PW_TEST_EXPECT_OK(SendL2capConfigureReq( |
| proxy, Direction::kFromController, kHandle, kLocalCid, l2cap_options)); |
| |
| PW_TEST_EXPECT_OK( |
| SendL2capConfigureRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kRemoteCid, |
| emboss::L2capConfigurationResult::SUCCESS)); |
| ASSERT_EQ(this->configuration_called, 2); |
| |
| proxy.UnregisterL2capStatusDelegate(*this); |
| } |
| |
| TEST_F(L2capStatusTrackerTest, L2capConfigurationNoOption) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| proxy_ptr = &proxy; |
| |
| constexpr uint16_t kSourceCid = 30; |
| constexpr uint16_t kDestinationCid = 31; |
| constexpr uint16_t kHandle = 123; |
| |
| proxy.RegisterL2capStatusDelegate(*this); |
| |
| PW_TEST_EXPECT_OK( |
| SendConnectionCompleteEvent(proxy, kHandle, emboss::StatusCode::SUCCESS)); |
| |
| // Send new connection req |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromController, kHandle, kSourceCid, kPsm)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send success rsp |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| |
| // Send Configure Request |
| auto expected_l2cap_configuration = L2capChannelConfigurationInfo{ |
| .direction = Direction::kFromController, |
| .connection_handle = kHandle, |
| .remote_cid = kSourceCid, |
| .local_cid = kDestinationCid, |
| .mtu = std::nullopt, |
| }; |
| |
| config_info.emplace(expected_l2cap_configuration); |
| |
| auto l2cap_options = L2capOptions{ |
| .mtu = std::nullopt, |
| }; |
| |
| PW_TEST_EXPECT_OK(SendL2capConfigureReq(proxy, |
| Direction::kFromController, |
| kHandle, |
| kDestinationCid, |
| l2cap_options)); |
| |
| PW_TEST_EXPECT_OK( |
| SendL2capConfigureRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kSourceCid, |
| emboss::L2capConfigurationResult::SUCCESS)); |
| |
| proxy.UnregisterL2capStatusDelegate(*this); |
| } |
| |
| TEST_F(L2capStatusTrackerTest, L2capEventsControllerInitiated) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| proxy_ptr = &proxy; |
| |
| constexpr uint16_t kSourceCid = 30; |
| constexpr uint16_t kDestinationCid = 31; |
| constexpr uint16_t kHandle = 123; |
| |
| proxy.RegisterL2capStatusDelegate(*this); |
| |
| PW_TEST_EXPECT_OK( |
| SendConnectionCompleteEvent(proxy, kHandle, emboss::StatusCode::SUCCESS)); |
| |
| // First send CONNECTION_REQ to setup partial connection |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromController, kHandle, kSourceCid, kPsm)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send non-successful connection response. |
| PW_TEST_EXPECT_OK(SendL2capConnectionRsp( |
| proxy, |
| Direction::kFromHost, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::INVALID_SOURCE_CID)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send successful connection response, but expect that it will not have |
| // called listener since the connection was closed with error already. |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send new connection req |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromController, kHandle, kSourceCid, kPsm)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send rsp with PENDING set. |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::PENDING)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send success rsp |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| EXPECT_TRUE(info.has_value()); |
| EXPECT_EQ(info->direction, Direction::kFromController); |
| EXPECT_EQ(info->connection_handle, kHandle); |
| EXPECT_EQ(info->local_cid, kDestinationCid); |
| EXPECT_EQ(info->remote_cid, kSourceCid); |
| |
| // Send disconnect |
| PW_TEST_EXPECT_OK(SendL2capDisconnectRsp(proxy, |
| Direction::kFromHost, |
| AclTransportType::kBrEdr, |
| kHandle, |
| kSourceCid, |
| kDestinationCid)); |
| EXPECT_FALSE(info.has_value()); |
| |
| proxy.UnregisterL2capStatusDelegate(*this); |
| |
| // Send successful connection sequence with no listeners. |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromController, kHandle, kSourceCid, kPsm)); |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromHost, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| EXPECT_FALSE(info.has_value()); |
| } |
| |
| TEST_F(L2capStatusTrackerTest, L2capEventsHostInitiated) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| []([[maybe_unused]] H4PacketWithH4&& packet) {}); |
| |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| []([[maybe_unused]] H4PacketWithHci&& packet) {}); |
| |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| proxy_ptr = &proxy; |
| |
| constexpr uint16_t kSourceCid = 30; |
| constexpr uint16_t kDestinationCid = 31; |
| constexpr uint16_t kHandle = 123; |
| |
| proxy.RegisterL2capStatusDelegate(*this); |
| |
| PW_TEST_EXPECT_OK( |
| SendConnectionCompleteEvent(proxy, kHandle, emboss::StatusCode::SUCCESS)); |
| |
| // First send CONNECTION_REQ to setup partial connection |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromHost, kHandle, kSourceCid, kPsm)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send non-successful connection response. |
| PW_TEST_EXPECT_OK(SendL2capConnectionRsp( |
| proxy, |
| Direction::kFromController, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::INVALID_SOURCE_CID)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send successful connection response, but expect that it will not have |
| // called listener since the connection was closed with error already. |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromController, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send new connection req |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromHost, kHandle, kSourceCid, kPsm)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send rsp with PENDING set. |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromController, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::PENDING)); |
| EXPECT_FALSE(info.has_value()); |
| |
| // Send success rsp |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromController, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| EXPECT_TRUE(info.has_value()); |
| EXPECT_EQ(info->direction, Direction::kFromHost); |
| EXPECT_EQ(info->connection_handle, kHandle); |
| EXPECT_EQ(info->local_cid, kSourceCid); |
| EXPECT_EQ(info->remote_cid, kDestinationCid); |
| |
| // Send disconnect rsp |
| PW_TEST_EXPECT_OK(SendL2capDisconnectRsp(proxy, |
| Direction::kFromController, |
| AclTransportType::kBrEdr, |
| kHandle, |
| kSourceCid, |
| kDestinationCid)); |
| EXPECT_FALSE(info.has_value()); |
| |
| proxy.UnregisterL2capStatusDelegate(*this); |
| |
| // Send successful connection sequence with no listeners. |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq( |
| proxy, Direction::kFromHost, kHandle, kSourceCid, kPsm)); |
| PW_TEST_EXPECT_OK( |
| SendL2capConnectionRsp(proxy, |
| Direction::kFromController, |
| kHandle, |
| kSourceCid, |
| kDestinationCid, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| EXPECT_FALSE(info.has_value()); |
| } |
| |
| TEST_F(ProxyHostConnectionEventTest, HciDisconnectionAlertsListeners) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| constexpr uint16_t kPsm = 1; |
| |
| class TestStatusDelegate final : public L2capStatusDelegate { |
| public: |
| bool ShouldTrackPsm(uint16_t psm) override { return psm == kPsm; } |
| void HandleConnectionComplete(const L2capChannelConnectionInfo&) override { |
| ++connections_received; |
| } |
| void HandleDisconnectionComplete( |
| const L2capChannelConnectionInfo&) override { |
| ++disconnections_received; |
| } |
| |
| void HandleConfigurationChanged( |
| const L2capChannelConfigurationInfo&) override { |
| ++configuration_received; |
| } |
| |
| int connections_received = 0; |
| int disconnections_received = 0; |
| int configuration_received = 0; |
| }; |
| |
| TestStatusDelegate test_delegate; |
| proxy.RegisterL2capStatusDelegate(test_delegate); |
| |
| constexpr uint16_t Handle1 = 0x123, Handle2 = 0x124; |
| PW_TEST_EXPECT_OK( |
| SendConnectionCompleteEvent(proxy, Handle1, emboss::StatusCode::SUCCESS)); |
| PW_TEST_EXPECT_OK( |
| SendConnectionCompleteEvent(proxy, Handle2, emboss::StatusCode::SUCCESS)); |
| |
| // Establish three connected_channels: |
| // handle = 0x123, PSM = 1 | handle = 0x124, PSM = 1 | handle = 0x123, PSM = |
| // 1 |
| constexpr uint16_t kStartSourceCid = 0x111; |
| constexpr uint16_t kStartDestinationCid = 0x211; |
| |
| auto l2cap_options = L2capOptions{ |
| .mtu = MtuOption{1024}, |
| }; |
| |
| for (size_t i = 0; i < 3; ++i) { |
| PW_TEST_EXPECT_OK(SendL2capConnectionReq(proxy, |
| Direction::kFromController, |
| i == 1 ? Handle2 : Handle1, |
| kStartSourceCid + i, |
| kPsm)); |
| PW_TEST_EXPECT_OK(SendL2capConnectionRsp( |
| proxy, |
| Direction::kFromHost, |
| i == 1 ? Handle2 : Handle1, |
| kStartSourceCid + i, |
| kStartDestinationCid + i, |
| emboss::L2capConnectionRspResultCode::SUCCESSFUL)); |
| PW_TEST_EXPECT_OK(SendL2capConfigureReq(proxy, |
| Direction::kFromController, |
| i == 1 ? Handle2 : Handle1, |
| kStartDestinationCid + i, |
| l2cap_options)); |
| PW_TEST_EXPECT_OK( |
| SendL2capConfigureRsp(proxy, |
| Direction::kFromHost, |
| i == 1 ? Handle2 : Handle1, |
| kStartSourceCid + i, |
| emboss::L2capConfigurationResult::SUCCESS)); |
| } |
| |
| EXPECT_EQ(test_delegate.connections_received, 3); |
| EXPECT_EQ(test_delegate.configuration_received, 3); |
| EXPECT_EQ(test_delegate.disconnections_received, 0); |
| |
| // Disconnect handle1, which should disconnect first and third channel. |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, Handle1)); |
| EXPECT_EQ(test_delegate.disconnections_received, 2); |
| |
| // Confirm remaining channel can still be disconnected properly. |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, Handle2)); |
| EXPECT_EQ(test_delegate.disconnections_received, 3); |
| |
| proxy.UnregisterL2capStatusDelegate(test_delegate); |
| } |
| |
| TEST_F(ProxyHostConnectionEventTest, |
| HciDisconnectionFromControllerClosesChannels) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| constexpr uint16_t kHandle = 0x123; |
| constexpr uint16_t kStartingCid = 0x111; |
| int events_received = 0; |
| auto event_fn = [&events_received](L2capChannelEvent event) { |
| ++events_received; |
| EXPECT_EQ(event, L2capChannelEvent::kChannelClosedByOther); |
| }; |
| BasicL2capChannel chan1 = BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingCid, |
| .remote_cid = kStartingCid, |
| .event_fn = event_fn}); |
| // chan2 is on a different connection so should not be closed |
| BasicL2capChannel chan2 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle + 1, |
| .local_cid = kStartingCid + 1, |
| .remote_cid = kStartingCid + 1, |
| .event_fn = event_fn}); |
| BasicL2capChannel chan3 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingCid + 2, |
| .remote_cid = kStartingCid + 2, |
| .event_fn = event_fn}); |
| |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kRunning); |
| |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, kHandle)); |
| |
| EXPECT_EQ(events_received, 2); |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kClosed); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kClosed); |
| |
| // Confirm L2CAP_DISCONNECTION_RSP packet does not result in another event. |
| PW_TEST_EXPECT_OK(SendL2capDisconnectRsp(proxy, |
| Direction::kFromHost, |
| AclTransportType::kLe, |
| kHandle, |
| kStartingCid, |
| kStartingCid)); |
| EXPECT_EQ(events_received, 2); |
| } |
| |
| TEST_F(ProxyHostConnectionEventTest, |
| L2capDisconnectionRspFromHostClosesChannels) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| constexpr uint16_t kHandle = 0x123; |
| constexpr uint16_t kStartingSourceCid = 0x111; |
| constexpr uint16_t kStartingDestinationCid = 0x211; |
| int events_received = 0; |
| auto event_fn = [&events_received](L2capChannelEvent event) { |
| ++events_received; |
| EXPECT_EQ(event, L2capChannelEvent::kChannelClosedByOther); |
| }; |
| BasicL2capChannel chan1 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingDestinationCid, |
| .remote_cid = kStartingSourceCid, |
| .event_fn = event_fn}); |
| BasicL2capChannel chan2 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingDestinationCid + 1, |
| .remote_cid = kStartingSourceCid + 1, |
| .event_fn = event_fn}); |
| BasicL2capChannel chan3 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingDestinationCid + 2, |
| .remote_cid = kStartingSourceCid + 2, |
| .event_fn = event_fn}); |
| |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kRunning); |
| |
| // Close chan1's & chan2's underlying L2CAP connections. |
| PW_TEST_EXPECT_OK( |
| SendL2capDisconnectRsp(proxy, |
| Direction::kFromHost, |
| AclTransportType::kLe, |
| kHandle, |
| /*source_cid=*/kStartingSourceCid, |
| /*destination_cid=*/kStartingDestinationCid)); |
| PW_TEST_EXPECT_OK( |
| SendL2capDisconnectRsp(proxy, |
| Direction::kFromHost, |
| AclTransportType::kLe, |
| kHandle, |
| /*source_cid=*/kStartingSourceCid + 2, |
| /*destination_cid=*/kStartingDestinationCid + 2)); |
| |
| EXPECT_EQ(events_received, 2); |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kClosed); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kClosed); |
| |
| // Confirm HCI disconnection only closes remaining channel. |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, kHandle)); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kClosed); |
| EXPECT_EQ(events_received, 3); |
| } |
| |
| TEST_F(ProxyHostConnectionEventTest, HciDisconnectionFromHostClosesChannels) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| constexpr uint16_t kHandle = 0x123; |
| constexpr uint16_t kStartingCid = 0x111; |
| int events_received = 0; |
| auto event_fn = [&events_received](L2capChannelEvent event) { |
| ++events_received; |
| EXPECT_EQ(event, L2capChannelEvent::kChannelClosedByOther); |
| }; |
| BasicL2capChannel chan1 = BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingCid, |
| .remote_cid = kStartingCid, |
| .event_fn = event_fn}); |
| BasicL2capChannel chan2 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle + 1, |
| .local_cid = kStartingCid + 1, |
| .remote_cid = kStartingCid + 1, |
| .event_fn = event_fn}); |
| BasicL2capChannel chan3 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingCid + 2, |
| .remote_cid = kStartingCid + 2, |
| .event_fn = event_fn}); |
| |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kRunning); |
| |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent( |
| proxy, kHandle, /*direction=*/Direction::kFromHost)); |
| |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kClosed); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kClosed); |
| EXPECT_EQ(events_received, 2); |
| } |
| |
| TEST_F(ProxyHostConnectionEventTest, |
| L2capDisconnectionRspFromControllerClosesChannels) { |
| pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn( |
| [](H4PacketWithH4&&) {}); |
| pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn( |
| [](H4PacketWithHci&&) {}); |
| ProxyHost proxy = ProxyHost(std::move(send_to_host_fn), |
| std::move(send_to_controller_fn), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| |
| constexpr uint16_t kHandle = 0x123; |
| constexpr uint16_t kStartingCid = 0x111; |
| int events_received = 0; |
| auto event_fn = [&events_received](L2capChannelEvent event) { |
| ++events_received; |
| EXPECT_EQ(event, L2capChannelEvent::kChannelClosedByOther); |
| }; |
| BasicL2capChannel chan1 = BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingCid, |
| .remote_cid = kStartingCid, |
| .event_fn = event_fn}); |
| BasicL2capChannel chan2 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingCid + 1, |
| .remote_cid = kStartingCid + 1, |
| .event_fn = event_fn}); |
| BasicL2capChannel chan3 = |
| BuildBasicL2capChannel(proxy, |
| {.handle = kHandle, |
| .local_cid = kStartingCid + 2, |
| .remote_cid = kStartingCid + 2, |
| .event_fn = event_fn}); |
| |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kRunning); |
| |
| // Close chan1's & chan2's underlying L2CAP connections. |
| PW_TEST_EXPECT_OK(SendL2capDisconnectRsp(proxy, |
| Direction::kFromController, |
| AclTransportType::kLe, |
| kHandle, |
| kStartingCid, |
| kStartingCid)); |
| PW_TEST_EXPECT_OK(SendL2capDisconnectRsp(proxy, |
| Direction::kFromController, |
| AclTransportType::kLe, |
| kHandle, |
| kStartingCid + 2, |
| kStartingCid + 2)); |
| |
| EXPECT_EQ(events_received, 2); |
| EXPECT_EQ(chan1.state(), L2capChannel::State::kClosed); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kRunning); |
| EXPECT_EQ(chan3.state(), L2capChannel::State::kClosed); |
| |
| // Confirm HCI disconnection only closes remaining channel. |
| PW_TEST_EXPECT_OK(SendDisconnectionCompleteEvent(proxy, kHandle)); |
| EXPECT_EQ(chan2.state(), L2capChannel::State::kClosed); |
| EXPECT_EQ(events_received, 3); |
| } |
| |
| // ########## AclFragTest |
| |
| class AclFragTest : public ProxyHostTest { |
| protected: |
| static constexpr uint16_t kHandle = 0x4AD; |
| static constexpr uint16_t kLocalCid = 0xC1D; |
| |
| ProxyHost GetProxy() { |
| // We can't add a ProxyHost member because it makes the test fixture too |
| // large, so we provide a helper function instead. |
| return ProxyHost(pw::bind_member<&AclFragTest::SendToHost>(this), |
| pw::bind_member<&AclFragTest::SendToController>(this), |
| /*le_acl_credits_to_reserve=*/0, |
| /*br_edr_acl_credits_to_reserve=*/0); |
| } |
| |
| BasicL2capChannel GetL2capChannel( |
| ProxyHost& proxy, |
| multibuf::MultiBufAllocator* rx_multibuf_allocator = nullptr) { |
| return BuildBasicL2capChannel( |
| proxy, |
| BasicL2capParameters{ |
| .rx_multibuf_allocator = rx_multibuf_allocator, |
| .handle = kHandle, |
| .local_cid = kLocalCid, |
| .remote_cid = 0x123, |
| .transport = AclTransportType::kLe, |
| .payload_from_controller_fn = |
| [this](multibuf::MultiBuf&& buffer) { |
| payloads_from_controller_.emplace_back(std::move(buffer)); |
| return std::nullopt; // Consume |
| }, |
| }); |
| } |
| |
| // Verify the payloads the client received. |
| // Also dtor them (in some cases they may have been allocated in the test). |
| void ExpectClientReceivedPayloadsAndClear( |
| std::initializer_list<ConstByteSpan> expected_payloads) { |
| EXPECT_EQ(payloads_from_controller_.size(), expected_payloads.size()); |
| if (payloads_from_controller_.size() != expected_payloads.size()) { |
| return; |
| } |
| |
| auto payloads_iter = payloads_from_controller_.begin(); |
| for (ConstByteSpan expected : expected_payloads) { |
| std::optional<pw::ByteSpan> payload = (payloads_iter++)->ContiguousSpan(); |
| PW_CHECK(payload.has_value()); |
| EXPECT_TRUE(std::equal( |
| payload->begin(), payload->end(), expected.begin(), expected.end())); |
| } |
| payloads_from_controller_.clear(); |
| } |
| |
| void VerifyNormalOperationAfterRecombination(ProxyHost& proxy) { |
| // Verify things work normally after recombination ends. |
| static constexpr std::array<uint8_t, 4> kPayload = {'D', 'o', 'n', 'e'}; |
| payloads_from_controller_.clear(); |
| SendL2capBFrame(proxy, kHandle, kPayload, kPayload.size(), kLocalCid); |
| ExpectClientReceivedPayloadsAndClear({ |
| as_bytes(span(kPayload)), |
| }); |
| } |
| |
| int packets_sent_to_host_ = 0; |
| int packets_sent_to_controller_ = 0; |
| |
| private: |
| void SendToHost(H4PacketWithHci&& /*packet*/) { ++packets_sent_to_host_; } |
| |
| void SendToController(H4PacketWithH4&& /*packet*/) { |
| ++packets_sent_to_controller_; |
| } |
| |
| std::vector<multibuf::MultiBuf> payloads_from_controller_; |
| }; |
| |
| TEST_F(AclFragTest, AclBiggerThanL2capDropped) { |
| ProxyHost proxy = GetProxy(); |
| BasicL2capChannel channel = GetL2capChannel(proxy); |
| |
| // Send an ACL packet with more data than L2CAP header indicates. |
| static constexpr std::array<uint8_t, 4> kPayload{}; |
| SendL2capBFrame(proxy, kHandle, kPayload, 1, kLocalCid); |
| |
| // Should be dropped. |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| ExpectClientReceivedPayloadsAndClear({}); |
| } |
| |
| TEST_F(AclFragTest, RecombinationWorksWithEmptyFirstPayload) { |
| ProxyHost proxy = GetProxy(); |
| BasicL2capChannel channel = GetL2capChannel(proxy); |
| |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| // Fragment 1: ACL Header + L2CAP B-Frame Header + (no payload) |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header"); |
| SendL2capBFrame(proxy, kHandle, {}, kPayload.size(), kLocalCid); |
| |
| // Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2"); |
| SendAclContinuingFrag(proxy, kHandle, kPayload); |
| |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| ExpectClientReceivedPayloadsAndClear({ |
| as_bytes(span(kPayload)), |
| }); |
| |
| VerifyNormalOperationAfterRecombination(proxy); |
| } |
| |
| // If a client channel is dropped between first and last |
| // packet of a fragmented PDU, then packet should be dropped. |
| // Under msan this test also verifies code is not trying to access channel |
| // allocator's memory after channel dtor. |
| TEST_F(AclFragTest, ChannelDtorDuringRecombinationDropsPdu) { |
| ProxyHost proxy = GetProxy(); |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| { |
| pw::multibuf::test::SimpleAllocatorForTest</*kDataSizeBytes=*/1024, |
| /*kMetaSizeBytes=*/2 * 1024> |
| rx_allocator{}; |
| BasicL2capChannel channel = GetL2capChannel(proxy, &rx_allocator); |
| |
| // Fragment 1: ACL Header + L2CAP B-Frame Header + (no payload) |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header"); |
| |
| SendL2capBFrame(proxy, kHandle, {}, kPayload.size(), kLocalCid); |
| |
| // Dtor of channel and allocator. |
| } |
| |
| // Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2"); |
| // Since channel was destroyed before this, channel allocator's memory should |
| // not be accessed (msan will verify). |
| SendAclContinuingFrag(proxy, kHandle, kPayload); |
| |
| // Since channel was destroyed before 2nd fragment was sent, PDU should have |
| // been dropped. |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| ExpectClientReceivedPayloadsAndClear({}); |
| |
| // Open up channel again to verify rx still works after completing above. |
| BasicL2capChannel channel2 = GetL2capChannel(proxy); |
| VerifyNormalOperationAfterRecombination(proxy); |
| } |
| |
| // During recombination dtor first channel, but then create new channel with |
| // same cid. Verify recombination is properly dropped. |
| TEST_F(AclFragTest, ChannelDtorAndNewChannelDuringRecombination) { |
| ProxyHost proxy = GetProxy(); |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| { |
| pw::multibuf::test::SimpleAllocatorForTest</*kDataSizeBytes=*/1024, |
| /*kMetaSizeBytes=*/2 * 1024> |
| rx_allocator{}; |
| BasicL2capChannel channel = GetL2capChannel(proxy, &rx_allocator); |
| |
| // Fragment 1: ACL Header + L2CAP B-Frame Header + (no payload) |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header"); |
| |
| SendL2capBFrame(proxy, kHandle, {}, kPayload.size(), kLocalCid); |
| |
| // Dtor of channel and allocator. |
| } |
| |
| // Open up L2CAP channel with same channel id on same connection. |
| BasicL2capChannel channel2 = GetL2capChannel(proxy); |
| |
| // Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2"); |
| // Since channel1 was destroyed before this, channel1 allocator's |
| // memory should not be accessed (msan will verify). |
| SendAclContinuingFrag(proxy, kHandle, kPayload); |
| |
| // Since channel1 was destroyed before 2nd fragment was sent, its PDU should |
| // have been dropped even though channel2 with same cid was created. |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| ExpectClientReceivedPayloadsAndClear({}); |
| |
| // Verify rx to channel2 still works. |
| VerifyNormalOperationAfterRecombination(proxy); |
| } |
| |
| // Ensure expected handling of channel not having enough allocator space to fit |
| // the recombined buffer. Current behavior is to pass first and any continuing |
| // packets to AP. |
| // TODO: https://pwbug.dev/404275508 - We should probably do something different |
| // in this case (like stopping channel or at least sending it an event). |
| TEST_F(AclFragTest, ChannelCantAllocateMultibuf) { |
| // Intentionally use allocator without enough room for PDU buf. |
| pw::multibuf::test::SimpleAllocatorForTest</*kDataSizeBytes=*/1, |
| /*kMetaSizeBytes=*/2 * 1024> |
| rx_allocator{}; |
| ProxyHost proxy = GetProxy(); |
| BasicL2capChannel channel = GetL2capChannel(proxy, &rx_allocator); |
| |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| // Fragment 1: ACL Header + L2CAP B-Frame Header + (no payload) |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header"); |
| SendL2capBFrame(proxy, kHandle, {}, kPayload.size(), kLocalCid); |
| |
| // Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2"); |
| SendAclContinuingFrag(proxy, kHandle, kPayload); |
| |
| // Both packets should have been sent to host. |
| EXPECT_EQ(packets_sent_to_host_, 2); |
| // No payloads should have been sent to the client. |
| ExpectClientReceivedPayloadsAndClear({}); |
| } |
| |
| TEST_F(AclFragTest, RecombinationWorksWithSplitPayloads) { |
| ProxyHost proxy = GetProxy(); |
| BasicL2capChannel channel = GetL2capChannel(proxy); |
| |
| static constexpr std::array<uint8_t, 2> kPayloadFrag1 = {0xA1, 0xB2}; |
| static constexpr std::array<uint8_t, 2> kPayloadFrag2 = {0xC3, 0xD2}; |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| constexpr int kNumIter = 4; |
| |
| for (int i = 0; i < kNumIter; ++i) { |
| // Fragment 1: ACL Header + L2CAP B-Frame Header + Payload frag 1 |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header + payload1"); |
| SendL2capBFrame(proxy, kHandle, kPayloadFrag1, kPayload.size(), kLocalCid); |
| |
| // Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2"); |
| SendAclContinuingFrag(proxy, kHandle, kPayloadFrag2); |
| } |
| |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| ExpectClientReceivedPayloadsAndClear({ |
| as_bytes(span(kPayload)), |
| as_bytes(span(kPayload)), |
| as_bytes(span(kPayload)), |
| as_bytes(span(kPayload)), |
| }); |
| |
| VerifyNormalOperationAfterRecombination(proxy); |
| } |
| |
| TEST_F(AclFragTest, UnexpectedContinuingFragment) { |
| ProxyHost proxy = GetProxy(); |
| BasicL2capChannel channel = GetL2capChannel(proxy); |
| |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| // Send an unexpected CONTINUING_FRAGMENT |
| PW_LOG_INFO("Sending frag 1: ACL(CONT) + payload"); |
| SendAclContinuingFrag(proxy, kHandle, kPayload); |
| |
| ExpectClientReceivedPayloadsAndClear({}); |
| EXPECT_EQ(packets_sent_to_host_, 1); // Should be passed on to host |
| |
| VerifyNormalOperationAfterRecombination(proxy); |
| } |
| |
| TEST_F(AclFragTest, UnexpectedFirstFragment) { |
| ProxyHost proxy = GetProxy(); |
| BasicL2capChannel channel = GetL2capChannel(proxy); |
| |
| static constexpr std::array<uint8_t, 2> kPayloadFrag1 = {0xA1, 0xB2}; |
| static constexpr std::array<uint8_t, 2> kPayloadFrag2 = {0xC3, 0xD2}; |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| // PDU A: Fragment 1: Start recombination by sending first fragment. |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header + payload1"); |
| SendL2capBFrame(proxy, kHandle, {}, 100, kLocalCid); |
| |
| // We never send the 100 byte payload here. |
| |
| // So this new first-fragment is unexpected: |
| // PDU B: Fragment 1: ACL Header + L2CAP B-Frame Header + Payload frag 1 |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header + payload1"); |
| SendL2capBFrame(proxy, kHandle, kPayloadFrag1, kPayload.size(), kLocalCid); |
| |
| // PDU B: Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2"); |
| SendAclContinuingFrag(proxy, kHandle, kPayloadFrag2); |
| |
| // Nothing should be sent to the host. The first fragment of PDU A is dropped. |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| |
| // PDU B is delivered. |
| ExpectClientReceivedPayloadsAndClear({ |
| as_bytes(span(kPayload)), |
| }); |
| |
| VerifyNormalOperationAfterRecombination(proxy); |
| } |
| |
| TEST_F(AclFragTest, ContinuingFragmentTooLarge) { |
| ProxyHost proxy = GetProxy(); |
| BasicL2capChannel channel = GetL2capChannel(proxy); |
| |
| static constexpr std::array<uint8_t, 2> kPayloadFrag1 = {0xA1, 0xB2}; |
| static constexpr std::array<uint8_t, 5> kPayloadFrag2TooBig = { |
| 0xC3, 0xD2, 0xBA, 0xAA, 0xAD}; |
| static constexpr std::array<uint8_t, 4> kPayload = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| // Fragment 1: ACL Header + L2CAP B-Frame Header + Payload frag 1 |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header + payload1"); |
| SendL2capBFrame(proxy, kHandle, kPayloadFrag1, kPayload.size(), kLocalCid); |
| |
| // Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2 (too big)"); |
| SendAclContinuingFrag(proxy, kHandle, kPayloadFrag2TooBig); |
| |
| ExpectClientReceivedPayloadsAndClear({}); |
| |
| // This was for a channel owned by the proxy so it should have been dropped. |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| |
| VerifyNormalOperationAfterRecombination(proxy); |
| } |
| |
| TEST_F(AclFragTest, |
| CanReceiveUnfragmentedPduOnOneChannelWhileRecombiningOnAnother) { |
| ProxyHost proxy = GetProxy(); |
| |
| // Channel 1 |
| static constexpr std::array<uint8_t, 2> kPayload1Frag1 = {0xA1, 0xB2}; |
| static constexpr std::array<uint8_t, 2> kPayload1Frag2 = {0xC3, 0xD2}; |
| static constexpr std::array<uint8_t, 4> kPayload1 = {0xA1, 0xB2, 0xC3, 0xD2}; |
| |
| int channel1_sends_called = 0; |
| BasicL2capChannel channel = BuildBasicL2capChannel( |
| proxy, |
| BasicL2capParameters{ |
| .handle = kHandle, |
| .local_cid = kLocalCid, |
| .remote_cid = 0x123, |
| .transport = AclTransportType::kLe, |
| .payload_from_controller_fn = |
| [&channel1_sends_called](multibuf::MultiBuf&& buffer) { |
| ++channel1_sends_called; |
| std::optional<pw::ByteSpan> payload = buffer.ContiguousSpan(); |
| ConstByteSpan expected_bytes = as_bytes(span(kPayload1)); |
| EXPECT_TRUE(payload.has_value()); |
| EXPECT_TRUE(std::equal(payload->begin(), |
| payload->end(), |
| expected_bytes.begin(), |
| expected_bytes.end())); |
| return std::nullopt; |
| }, |
| }); |
| |
| // Channel 2 |
| static constexpr uint16_t kHandle2 = 0x4D2; |
| static constexpr uint16_t kLocalCid2 = 0xC2D; |
| static constexpr std::array<uint8_t, 4> kPayload2 = {0x33, 0x66, 0x99, 0xCC}; |
| |
| int channel2_sends_called = 0; |
| BasicL2capChannel channel2 = BuildBasicL2capChannel( |
| proxy, |
| BasicL2capParameters{ |
| .handle = kHandle2, |
| .local_cid = kLocalCid2, |
| .remote_cid = 0x321, |
| .transport = AclTransportType::kLe, |
| .payload_from_controller_fn = |
| [&channel2_sends_called](multibuf::MultiBuf&& buffer) { |
| ++channel2_sends_called; |
| std::optional<pw::ByteSpan> payload = buffer.ContiguousSpan(); |
| ConstByteSpan expected_bytes = as_bytes(span(kPayload2)); |
| EXPECT_TRUE(payload.has_value()); |
| EXPECT_TRUE(std::equal(payload->begin(), |
| payload->end(), |
| expected_bytes.begin(), |
| expected_bytes.end())); |
| return std::nullopt; |
| }, |
| }); |
| |
| // Channel 1: Fragment 1: ACL Header + L2CAP B-Frame Header + Payload frag 1 |
| PW_LOG_INFO("Sending frag 1: ACL + L2CAP header + payload1"); |
| SendL2capBFrame(proxy, kHandle, kPayload1Frag1, kPayload1.size(), kLocalCid); |
| |
| // Channel 2: Send full PDU |
| SendL2capBFrame(proxy, kHandle2, kPayload2, kPayload2.size(), kLocalCid2); |
| EXPECT_EQ(channel2_sends_called, 1); |
| |
| // Channel 1: Fragment 2: ACL Header + Payload frag 2 |
| PW_LOG_INFO("Sending frag 2: ACL(CONT) + payload2"); |
| SendAclContinuingFrag(proxy, kHandle, kPayload1Frag2); |
| |
| EXPECT_EQ(channel1_sends_called, 1); |
| EXPECT_EQ(packets_sent_to_host_, 0); |
| } |
| |
| } // namespace |
| } // namespace pw::bluetooth::proxy |