| // Copyright 2025 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_bluetooth_proxy/hci/command_multiplexer.h" |
| |
| #include <algorithm> |
| #include <chrono> |
| #include <cstdint> |
| |
| #include "pw_allocator/testing.h" |
| #include "pw_assert/check.h" |
| #include "pw_async2/dispatcher_for_test.h" |
| #include "pw_async2/func_task.h" |
| #include "pw_async2/simulated_time_provider.h" |
| #include "pw_bluetooth/hci_h4.emb.h" |
| #include "pw_containers/vector.h" |
| #include "pw_function/function.h" |
| #include "pw_status/try.h" |
| #include "pw_unit_test/framework.h" |
| |
| namespace pw::bluetooth::proxy::hci { |
| namespace { |
| |
| constexpr size_t kCommandCompleteHeaderSize = 6; |
| constexpr size_t kCommandCompleteParameterSize = 3; |
| constexpr size_t kAllocatorSize = 2048; |
| |
| struct GiveCommandCreditParams { |
| // An opcode of 0 is used to indicate commands can/cannot be sent without |
| // necessarily being associated with a particular command. |
| uint16_t opcode = 0; |
| |
| // By default allow a single additional command to be sent. |
| uint8_t count = 1; |
| |
| // By default, remove the resulting event from the host buffer. |
| bool remove_from_host_buffer = true; |
| }; |
| |
| template <size_t kPayloadSize = 0> |
| std::array<std::byte, kCommandCompleteHeaderSize + kPayloadSize> |
| MakeCommandCompletePacket(uint16_t opcode, |
| uint8_t credits, |
| span<const std::byte, kPayloadSize> payload = {}) { |
| static_assert(kPayloadSize + kCommandCompleteParameterSize <= |
| std::numeric_limits<uint8_t>::max()); |
| std::byte opcode_lower{static_cast<uint8_t>(opcode >> 0 & 0xFF)}; |
| std::byte opcode_upper{static_cast<uint8_t>(opcode >> 8 & 0xFF)}; |
| std::array<std::byte, kCommandCompleteHeaderSize + kPayloadSize> packet{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Command Complete) |
| std::byte(0x0E), |
| // Parameter size |
| std::byte(kCommandCompleteParameterSize + kPayloadSize), |
| // Num_HCI_Command_Packets |
| std::byte(credits), |
| // OpCode |
| opcode_lower, |
| opcode_upper, |
| }; |
| std::copy_n(payload.begin(), |
| payload.size(), |
| packet.begin() + kCommandCompleteHeaderSize); |
| return packet; |
| } |
| |
| TEST(IdentifierTest, UniqueIdentifier) { |
| using Int = uint8_t; |
| constexpr size_t kMax = std::numeric_limits<Int>::max(); |
| |
| IdentifierMint<uint8_t> mint; |
| pw::Vector<Identifier<Int>, kMax> ids; |
| for (size_t i = 0; i < kMax; ++i) { |
| auto new_id = mint.MintId([&](Int candidate) { |
| return std::find(ids.begin(), ids.end(), candidate) != ids.end(); |
| }); |
| ASSERT_TRUE(new_id.has_value()); |
| ASSERT_TRUE(new_id->is_valid()); |
| |
| EXPECT_EQ(new_id->value(), i + 1); |
| EXPECT_EQ(std::find(ids.begin(), ids.end(), new_id->value()), ids.end()); |
| |
| ids.push_back(std::move(*new_id)); |
| EXPECT_FALSE(new_id->is_valid()); |
| } |
| |
| { |
| // Allocation exhausted, should return std::nullopt. |
| auto result = mint.MintId([&](Int candidate) { |
| return std::find(ids.begin(), ids.end(), candidate) != ids.end(); |
| }); |
| EXPECT_FALSE(result.has_value()); |
| } |
| |
| ids.erase(std::remove(ids.begin(), ids.end(), 42), ids.end()); |
| |
| { |
| // Allocation opened up at 42, confirm allocation. |
| auto result = mint.MintId([&](Int candidate) { |
| return std::find(ids.begin(), ids.end(), candidate) != ids.end(); |
| }); |
| ASSERT_TRUE(result.has_value()); |
| EXPECT_EQ(result->value(), 42); |
| ids.push_back(std::move(*result)); |
| } |
| |
| { |
| // Allocation exhausted again, should return std::nullopt. |
| auto result = mint.MintId([&](Int candidate) { |
| return std::find(ids.begin(), ids.end(), candidate) != ids.end(); |
| }); |
| EXPECT_FALSE(result.has_value()); |
| } |
| } |
| |
| class CommandMultiplexerTest : public ::testing::Test { |
| public: |
| static constexpr Clock::duration kTestTimeoutDuration = |
| std::chrono::milliseconds(2); |
| |
| // Because we want to test both kinds of CommandMultiplexer (async vs cb), |
| // some tests are implemented outside the fixture, this provides access to the |
| // appropriate properties and functions. |
| // |
| // This is required because not all pw_unit_test backends support `TEST_P` |
| // style test parameterization. |
| class Accessor { |
| public: |
| CommandMultiplexer& hci_cmd_mux() { return hci_cmd_mux_; } |
| allocator::test::AllocatorForTest<kAllocatorSize>& allocator() { |
| return test_.allocator(); |
| } |
| async2::DispatcherForTest& dispatcher() { return test_.dispatcher(); } |
| |
| void set_auto_command_complete(bool value = true) { |
| auto_command_complete_ = value; |
| } |
| |
| pw::DynamicDeque<MultiBuf::Instance>& packets_to_host() { |
| return test_.packets_to_host(); |
| } |
| pw::DynamicDeque<MultiBuf::Instance>& packets_to_controller() { |
| return test_.packets_to_controller(); |
| } |
| |
| void SendFromHost(MultiBuf::Instance&& packet) { |
| bool give_credit = |
| auto_command_complete_ && *packet->begin() == std::byte(0x01); |
| hci_cmd_mux_.HandleH4FromHost(std::move(packet)); |
| if (give_credit) { |
| GiveCommandCredit(); |
| } |
| } |
| |
| void GiveCommandCredit(GiveCommandCreditParams params = {}) { |
| return test_.GiveCommandCredit(params); |
| } |
| |
| pw::Result<MultiBuf::Instance> AllocBuf(ConstByteSpan span) { |
| return test_.AllocBuf(span); |
| } |
| |
| private: |
| Accessor(CommandMultiplexerTest& test, CommandMultiplexer& hci_cmd_mux) |
| : test_(test), hci_cmd_mux_(hci_cmd_mux) {} |
| friend CommandMultiplexerTest; |
| |
| bool auto_command_complete_{false}; |
| CommandMultiplexerTest& test_; |
| CommandMultiplexer& hci_cmd_mux_; |
| }; |
| |
| protected: |
| CommandMultiplexer& hci_cmd_mux_async2() { |
| PW_CHECK(is_async_.value_or(true)); |
| is_async_ = true; |
| if (!cmd_mux_.has_value()) { |
| cmd_mux_.emplace(allocator_, |
| make_send_to_host_cb(), |
| make_send_to_controller_cb(), |
| time_provider_); |
| } |
| return *cmd_mux_; |
| } |
| |
| CommandMultiplexer& hci_cmd_mux_timer() { |
| PW_CHECK(!is_async_.value_or(false)); |
| is_async_ = false; |
| if (!cmd_mux_.has_value()) { |
| Function<void()> timeout_fn = [this] { OnTimeout(); }; |
| cmd_mux_.emplace(allocator_, |
| make_send_to_host_cb(), |
| make_send_to_controller_cb(), |
| std::move(timeout_fn), |
| kTestTimeoutDuration); |
| } |
| return *cmd_mux_; |
| } |
| |
| allocator::test::AllocatorForTest<kAllocatorSize>& allocator() { |
| return allocator_; |
| } |
| async2::DispatcherForTest& dispatcher() { return dispatcher_; } |
| |
| Accessor accessor_async2() { return Accessor(*this, hci_cmd_mux_async2()); } |
| |
| Accessor accessor_timer() { return Accessor(*this, hci_cmd_mux_timer()); } |
| |
| pw::DynamicDeque<MultiBuf::Instance>& packets_to_host() { |
| return packets_to_host_; |
| } |
| pw::DynamicDeque<MultiBuf::Instance>& packets_to_controller() { |
| return packets_to_controller_; |
| } |
| |
| pw::Result<MultiBuf::Instance> AllocBuf(ConstByteSpan span) { |
| MultiBuf::Instance buf(allocator()); |
| if (!buf->TryReserveForPushBack()) { |
| return Status::ResourceExhausted(); |
| } |
| |
| auto alloc = allocator().MakeUnique<std::byte[]>(span.size()); |
| if (alloc == nullptr) { |
| return Status::ResourceExhausted(); |
| } |
| |
| std::memcpy(alloc.get(), span.data(), span.size()); |
| buf->PushBack(std::move(alloc)); |
| return buf; |
| } |
| |
| void GiveCommandCredit(GiveCommandCreditParams params) { |
| auto host_packets_count = packets_to_host_.size(); |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto buf, |
| AllocBuf(MakeCommandCompletePacket(params.opcode, params.count))); |
| cmd_mux_->HandleH4FromController(std::move(buf)); |
| |
| PW_CHECK(packets_to_host_.size() == host_packets_count + 1); |
| if (params.remove_from_host_buffer) { |
| packets_to_host_.pop_back(); |
| } |
| } |
| |
| private: |
| void OnTimeout() {} |
| |
| Function<void(MultiBuf::Instance&&)> make_send_to_host_cb() { |
| return [this](MultiBuf::Instance&& packet) { |
| // Intentionally fail assert if allocation fails, this is test code. |
| packets_to_host_.push_back(std::move(packet)); |
| }; |
| } |
| |
| Function<void(MultiBuf::Instance&&)> make_send_to_controller_cb() { |
| return [this](MultiBuf::Instance&& packet) { |
| // Intentionally fail assert if allocation fails, this is test code. |
| packets_to_controller_.push_back(std::move(packet)); |
| }; |
| } |
| |
| async2::DispatcherForTest dispatcher_{}; |
| async2::SimulatedTimeProvider<Clock> time_provider_{}; |
| pw::allocator::test::AllocatorForTest<kAllocatorSize> allocator_{}; |
| |
| pw::DynamicDeque<MultiBuf::Instance> packets_to_host_{allocator_}; |
| pw::DynamicDeque<MultiBuf::Instance> packets_to_controller_{allocator_}; |
| |
| std::optional<CommandMultiplexer> cmd_mux_{std::nullopt}; |
| std::optional<bool> is_async_{std::nullopt}; |
| }; |
| |
| using Accessor = CommandMultiplexerTest::Accessor; |
| |
| TEST_F(CommandMultiplexerTest, AsyncTimeout) { |
| auto& hci_cmd_mux = hci_cmd_mux_async2(); |
| |
| std::optional<Result<async2::Poll<>>> pend_result; |
| async2::FuncTask task{[&](async2::Context& cx) { |
| pend_result = hci_cmd_mux.PendCommandTimeout(cx); |
| return async2::Ready(); |
| }}; |
| |
| dispatcher().Post(task); |
| dispatcher().RunToCompletion(); |
| |
| ASSERT_TRUE(pend_result.has_value()); |
| ASSERT_FALSE(pend_result->ok()); |
| // Not yet implemented. |
| EXPECT_EQ(pend_result->status(), Status::Unimplemented()); |
| } |
| |
| TEST_F(CommandMultiplexerTest, AsyncTimeoutFailsSync) { |
| auto& hci_cmd_mux = hci_cmd_mux_timer(); |
| |
| std::optional<Result<async2::Poll<>>> pend_result; |
| async2::FuncTask task{[&](async2::Context& cx) { |
| pend_result = hci_cmd_mux.PendCommandTimeout(cx); |
| return async2::Ready(); |
| }}; |
| |
| dispatcher().Post(task); |
| dispatcher().RunToCompletion(); |
| |
| ASSERT_TRUE(pend_result.has_value()); |
| ASSERT_FALSE(pend_result->ok()); |
| EXPECT_EQ(pend_result->status(), Status::Unimplemented()); |
| } |
| |
| void TestSendCommand(Accessor test) { |
| static constexpr std::array<std::byte, 3> reset_command_packet_bytes{ |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 4> inquiry_command_packet_bytes{ |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 7> inquiry_command_status_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Command Status) |
| std::byte(0x0F), |
| // Parameter size |
| std::byte(0x04), |
| // Status (Success) |
| std::byte(0x00), |
| // Num_HCI_Command_Packets |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| }; |
| |
| // Try sending empty buffer. |
| { |
| MultiBuf::Instance buffer(test.allocator()); |
| |
| auto result = test.hci_cmd_mux().SendCommand( |
| {std::move(buffer)}, |
| [](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().status(), Status::InvalidArgument()); |
| } |
| |
| // Try sending buffer containing a valid command. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_packet_bytes); |
| |
| bool event_handler_called = false; |
| auto result = test.hci_cmd_mux().SendCommand( |
| {std::move(buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| event_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE); |
| EXPECT_TRUE(result.has_value()); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> out_h4_header{}; |
| std::array<std::byte, reset_command_packet_bytes.size()> out_payload{}; |
| test.packets_to_controller().front()->CopyTo(out_h4_header); |
| test.packets_to_controller().front()->CopyTo(out_payload, 1); |
| EXPECT_EQ(out_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(reset_command_packet_bytes, out_payload); |
| test.packets_to_controller().pop_front(); |
| |
| EXPECT_FALSE(event_handler_called); |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto command_complete, |
| test.AllocBuf(MakeCommandCompletePacket(0x0C03, 1))); |
| test.hci_cmd_mux().HandleH4FromController(std::move(command_complete)); |
| EXPECT_TRUE(event_handler_called); |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| } |
| |
| test.GiveCommandCredit({.opcode = 0x0C03}); |
| |
| // Try sending buffer containing a valid command and expecting COMMAND_STATUS. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), inquiry_command_packet_bytes); |
| |
| bool event_handler_called = false; |
| auto result = test.hci_cmd_mux().SendCommand( |
| {std::move(buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| event_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_STATUS); |
| EXPECT_TRUE(result.has_value()); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> out_h4_header{}; |
| std::array<std::byte, inquiry_command_packet_bytes.size()> out_payload{}; |
| test.packets_to_controller().front()->CopyTo(out_h4_header); |
| test.packets_to_controller().front()->CopyTo(out_payload, 1); |
| EXPECT_EQ(out_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(inquiry_command_packet_bytes, out_payload); |
| test.packets_to_controller().pop_front(); |
| |
| EXPECT_FALSE(event_handler_called); |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto command_status, |
| test.AllocBuf(inquiry_command_status_packet_bytes)); |
| test.hci_cmd_mux().HandleH4FromController(std::move(command_status)); |
| EXPECT_TRUE(event_handler_called); |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| } |
| } |
| |
| TEST_F(CommandMultiplexerTest, SendCommandAsync) { |
| TestSendCommand(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, SendCommandTimer) { |
| TestSendCommand(accessor_timer()); |
| } |
| |
| void TestSendEvent(Accessor test) { |
| static constexpr std::array<std::byte, 5> command_complete_packet_bytes{ |
| // Event code (Command Complete) |
| std::byte(0x0E), |
| // Parameter size |
| std::byte(0x03), |
| // Num_HCI_Command_Packets |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| }; |
| |
| static constexpr std::array<std::byte, 4> hardware_error_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Hardware Error) |
| std::byte(0x10), |
| // Parameter size |
| std::byte(0x01), |
| // Hardware_Code |
| std::byte(0x01), |
| }; |
| |
| // Try sending empty buffer. |
| { |
| MultiBuf::Instance buffer(test.allocator()); |
| |
| auto result = test.hci_cmd_mux().SendEvent({std::move(buffer)}); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().status(), Status::InvalidArgument()); |
| } |
| |
| // Try sending buffer containing a valid event. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), command_complete_packet_bytes); |
| |
| auto result = test.hci_cmd_mux().SendEvent({std::move(buf)}); |
| EXPECT_TRUE(result.has_value()); |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 1> out_h4_header{}; |
| std::array<std::byte, command_complete_packet_bytes.size()> out_payload{}; |
| test.packets_to_host().front()->CopyTo(out_h4_header); |
| test.packets_to_host().front()->CopyTo(out_payload, 1); |
| EXPECT_EQ(out_h4_header[0], std::byte(emboss::H4PacketType::EVENT)); |
| EXPECT_EQ(command_complete_packet_bytes, out_payload); |
| test.packets_to_host().pop_front(); |
| } |
| |
| // Register an event interceptor and ensure that SendEvent bypasses it. |
| { |
| std::optional<EventPacket> intercepted; |
| auto result = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::HARDWARE_ERROR, |
| [&](EventPacket&& packet) |
| -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet); |
| return {}; |
| }); |
| EXPECT_EQ(result.status(), OkStatus()); |
| EXPECT_TRUE(result.ok()); |
| |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), hardware_error_packet_bytes); |
| |
| auto send_result = test.hci_cmd_mux().SendEvent({std::move(buf)}); |
| EXPECT_TRUE(send_result.has_value()); |
| |
| // SendEvent should bypass interceptors. |
| EXPECT_FALSE(intercepted.has_value()); |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 1> out_h4_header{}; |
| std::array<std::byte, hardware_error_packet_bytes.size()> out_payload{}; |
| test.packets_to_host().front()->CopyTo(out_h4_header); |
| test.packets_to_host().front()->CopyTo(out_payload, 1); |
| EXPECT_EQ(out_h4_header[0], std::byte(emboss::H4PacketType::EVENT)); |
| EXPECT_EQ(hardware_error_packet_bytes, out_payload); |
| test.packets_to_host().pop_front(); |
| } |
| } |
| |
| TEST_F(CommandMultiplexerTest, SendEventAsync) { |
| TestSendEvent(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, SendEventTimer) { |
| TestSendEvent(accessor_timer()); |
| } |
| |
| void TestRegisterEventInterceptor(Accessor test) { |
| // Register an interceptor. |
| auto result1 = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::HARDWARE_ERROR, nullptr); |
| EXPECT_TRUE(result1.ok()); |
| |
| // Register a second interceptor. |
| auto result2 = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::INQUIRY_COMPLETE, nullptr); |
| EXPECT_TRUE(result2.ok()); |
| |
| // Ensure we can't register to an already-existing interceptor. |
| auto result3 = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::HARDWARE_ERROR, nullptr); |
| ASSERT_FALSE(result3.ok()); |
| EXPECT_EQ(result3.status(), Status::AlreadyExists()); |
| |
| // Reset result1, allowing us to register a different interceptor for the |
| // same code. |
| result1 = Status::Cancelled(); |
| auto result4 = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::HARDWARE_ERROR, nullptr); |
| EXPECT_TRUE(result4.ok()); |
| |
| // Reset both active interceptors, clearing the maps. |
| result2 = Status::Cancelled(); |
| result4 = Status::Cancelled(); |
| |
| // Now register two more interceptors to ensure clearing the maps worked. |
| auto result5 = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::HARDWARE_ERROR, nullptr); |
| EXPECT_TRUE(result5.ok()); |
| auto result6 = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::INQUIRY_COMPLETE, nullptr); |
| EXPECT_TRUE(result6.ok()); |
| } |
| |
| TEST_F(CommandMultiplexerTest, RegisterEventInterceptorAsync) { |
| TestRegisterEventInterceptor(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, RegisterEventInterceptorTimer) { |
| TestRegisterEventInterceptor(accessor_timer()); |
| } |
| |
| void TestRegisterCommandInterceptor(Accessor test) { |
| // Register an interceptor. |
| auto result1 = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::INQUIRY, nullptr); |
| EXPECT_TRUE(result1.ok()); |
| |
| // Register a second interceptor. |
| auto result2 = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::DISCONNECT, nullptr); |
| EXPECT_TRUE(result2.ok()); |
| |
| // Ensure we can't register to an already-existing interceptor. |
| auto result3 = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::INQUIRY, nullptr); |
| ASSERT_FALSE(result3.ok()); |
| EXPECT_EQ(result3.status(), Status::AlreadyExists()); |
| |
| // Reset result1, allowing us to register a different interceptor for the |
| // same code. |
| result1 = Status::Cancelled(); |
| auto result4 = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::INQUIRY, nullptr); |
| EXPECT_TRUE(result4.ok()); |
| |
| // Reset both active interceptors, clearing the maps. |
| result2 = Status::Cancelled(); |
| result4 = Status::Cancelled(); |
| |
| // Now register two more interceptors to ensure clearing the maps worked. |
| auto result5 = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::INQUIRY, nullptr); |
| EXPECT_TRUE(result5.ok()); |
| auto result6 = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::DISCONNECT, nullptr); |
| EXPECT_TRUE(result6.ok()); |
| } |
| |
| TEST_F(CommandMultiplexerTest, RegisterCommandInterceptorAsync) { |
| TestRegisterCommandInterceptor(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, RegisterCommandInterceptorTimer) { |
| TestRegisterCommandInterceptor(accessor_timer()); |
| } |
| |
| void TestInterceptCommands(Accessor test) { |
| static constexpr std::array<std::byte, 4> reset_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| test.set_auto_command_complete(); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 4> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| // Register an interceptor (takes buffer, continues intercepting) |
| std::optional<MultiBuf::Instance> intercepted; |
| auto result = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::RESET, |
| [&](CommandPacket&& packet) |
| -> CommandMultiplexer::CommandInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| ASSERT_EQ(result.status(), OkStatus()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 4> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| } |
| |
| // Clear the result, but keep interceptor active. |
| intercepted = std::nullopt; |
| |
| // Ensure continuing to intercept. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 4> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| } |
| |
| result = Status::Cancelled(); // Unregister the interceptor. |
| intercepted = std::nullopt; |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| EXPECT_FALSE(intercepted.has_value()); |
| std::array<std::byte, 4> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| // Register an interceptor (Does not take buffer, continues intercepting) |
| bool peeked = false; |
| result = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::RESET, |
| [&](CommandPacket&& packet) |
| -> CommandMultiplexer::CommandInterceptorReturn { |
| peeked = true; |
| return {std::move(packet)}; |
| }); |
| ASSERT_TRUE(result.ok()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Forwards packets returned from interceptor. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| EXPECT_TRUE(peeked); |
| std::array<std::byte, 4> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| peeked = false; |
| |
| // Ensure continuing to intercept. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Forwards packets returned from interceptor. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| EXPECT_TRUE(peeked); |
| std::array<std::byte, 4> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| // Replace with an interceptor that removes itself. |
| result = Status::Cancelled(); |
| struct { |
| decltype(result)& result_; |
| decltype(intercepted)& intercepted_; |
| } capture{ |
| .result_ = result, |
| .intercepted_ = intercepted, |
| }; |
| result = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::RESET, |
| [&capture](CommandPacket&& packet) |
| -> CommandMultiplexer::CommandInterceptorReturn { |
| capture.intercepted_ = std::move(packet.buffer); |
| return {.action = CommandMultiplexer::RemoveThisInterceptor{ |
| std::move(capture.result_.value().id())}}; |
| }); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 4> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| } |
| |
| intercepted = std::nullopt; |
| |
| // Ensure the next one is not intercepted. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| EXPECT_FALSE(intercepted.has_value()); |
| std::array<std::byte, 4> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| } |
| } |
| |
| TEST_F(CommandMultiplexerTest, InterceptCommandsAsync) { |
| TestInterceptCommands(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, InterceptCommandsTimer) { |
| TestInterceptCommands(accessor_timer()); |
| } |
| |
| void TestInterceptEvents(Accessor test) { |
| static constexpr std::array<std::byte, 6> reset_command_complete_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Command Complete) |
| std::byte(0x0E), |
| // Parameter size |
| std::byte(0x03), |
| // Num_HCI_Command_Packets |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| }; |
| static constexpr std::array<std::byte, 6> vendor_debug_subevent1_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Vendor Debug) |
| std::byte(0xFF), |
| // Parameter size |
| std::byte(0x03), |
| // Subevent code (0x01) |
| std::byte(0x01), |
| std::byte(0x00), |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 6> vendor_debug_subevent2_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Vendor Debug) |
| std::byte(0xFF), |
| // Parameter size |
| std::byte(0x03), |
| // Subevent code (0x02) |
| std::byte(0x02), |
| std::byte(0x00), |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 6> le_meta_subevent1_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code |
| std::byte(0x3E), |
| // Parameter size |
| std::byte(0x03), |
| // Subevent code (0x01) |
| std::byte(0x01), |
| std::byte(0x00), |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 6> le_meta_subevent2_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code |
| std::byte(0x3E), |
| // Parameter size |
| std::byte(0x03), |
| // Subevent code (0x02) |
| std::byte(0x02), |
| std::byte(0x00), |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 6> |
| inquiry_command_complete_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Command Complete) |
| std::byte(0x0E), |
| // Parameter size |
| std::byte(0x03), |
| // Num_HCI_Command_Pack |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| }; |
| static constexpr std::array<std::byte, 7> inquiry_command_status_packet_bytes{ |
| // Packet type (event) |
| std::byte(0x04), |
| // Event code (Command Status) |
| std::byte(0x0F), |
| // Parameter size |
| std::byte(0x04), |
| // Status (Success) |
| std::byte(0x00), |
| // Num_HCI_Command_Packets |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| }; |
| static constexpr std::array<std::byte, 7> |
| disconnect_command_status_packet_bytes{ |
| // Packet type |
| std::byte(0x04), |
| // Event code (Command Status) |
| std::byte(0x0F), |
| // Parameter size |
| std::byte(0x04), |
| // Status (Success) |
| std::byte(0x00), |
| // Num_HCI_Command_Packets |
| std::byte(0x01), |
| // OpCode (Disconnect) |
| std::byte(0x06), |
| std::byte(0x0C), |
| }; |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| // Register an interceptor (takes buffer, continues intercepting) |
| std::optional<MultiBuf::Instance> intercepted; |
| auto result = test.hci_cmd_mux().RegisterEventInterceptor( |
| CommandCompleteOpcode{emboss::OpCode::RESET}, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| ASSERT_TRUE(result.ok()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 6> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| } |
| |
| // Clear the result, but keep interceptor active. |
| intercepted = std::nullopt; |
| |
| // Ensure continuing to intercept. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 6> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| } |
| |
| result = Status::Cancelled(); // Unregister the interceptor. |
| intercepted = std::nullopt; |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| EXPECT_FALSE(intercepted.has_value()); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| // Register an interceptor (Does not take buffer, continues intercepting) |
| bool peeked = false; |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| CommandCompleteOpcode{emboss::OpCode::RESET}, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| peeked = true; |
| return {std::move(packet)}; |
| }); |
| ASSERT_TRUE(result.ok()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets returned from interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| EXPECT_TRUE(peeked); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| peeked = false; |
| |
| // Ensure continuing to intercept. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets returned from interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| EXPECT_TRUE(peeked); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| // Replace with an interceptor that removes itself. |
| result = Status::Cancelled(); |
| struct { |
| decltype(result)& result_; |
| decltype(intercepted)& intercepted_; |
| } capture{ |
| .result_ = result, |
| .intercepted_ = intercepted, |
| }; |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| CommandCompleteOpcode{emboss::OpCode::RESET}, |
| [&capture]( |
| EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| capture.intercepted_ = std::move(packet.buffer); |
| return {.action = CommandMultiplexer::RemoveThisInterceptor{ |
| std::move(capture.result_.value().id())}}; |
| }); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 6> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| } |
| |
| intercepted = std::nullopt; |
| |
| // Ensure the next one is not intercepted. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| EXPECT_FALSE(intercepted.has_value()); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(reset_command_complete_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| result = Status::Cancelled(); // Unregister the interceptor. |
| // Try to register an interceptor for Vendor Debug. |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::VENDOR_DEBUG, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| |
| // Should fail, not allowed. |
| EXPECT_FALSE(result.ok()); |
| EXPECT_EQ(result.status(), Status::InvalidArgument()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), vendor_debug_subevent1_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(vendor_debug_subevent1_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| result = Status::Cancelled(); // Unregister the interceptor. |
| // Register an interceptor for vendor debug subevent 0x01. |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| VendorDebugSubEventCode{0x01}, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| ASSERT_TRUE(result.ok()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), vendor_debug_subevent1_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 6> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(vendor_debug_subevent1_packet_bytes, out); |
| } |
| |
| intercepted = std::nullopt; |
| |
| // Test a different vendor debug subevent code (should not be intercepted). |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), vendor_debug_subevent2_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(vendor_debug_subevent2_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| // Test LE Meta Event subevent code. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), le_meta_subevent1_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(le_meta_subevent1_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| result = Status::Cancelled(); // Unregister the interceptor. |
| // Try to register an interceptor for LE Meta Event. |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::LE_META_EVENT, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| |
| // Should fail, not allowed. |
| EXPECT_FALSE(result.ok()); |
| EXPECT_EQ(result.status(), Status::InvalidArgument()); |
| |
| result = Status::Cancelled(); |
| // Register an interceptor for LE Meta Event subevent 0x01. |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::LeSubEventCode::CONNECTION_COMPLETE, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| ASSERT_TRUE(result.ok()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), le_meta_subevent1_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 6> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(le_meta_subevent1_packet_bytes, out); |
| } |
| |
| intercepted = std::nullopt; |
| |
| // Test a different LE Meta Event subevent code (should not be intercepted). |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), le_meta_subevent2_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Packet should not be intercepted. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(le_meta_subevent2_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| EXPECT_FALSE(intercepted.has_value()); |
| } |
| |
| intercepted = std::nullopt; |
| result = Status::Cancelled(); // Unregister the interceptor. |
| |
| // Try to register an interceptor for Command Complete event. |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::COMMAND_COMPLETE, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| |
| // Should fail, not allowed. |
| EXPECT_FALSE(result.ok()); |
| EXPECT_EQ(result.status(), Status::InvalidArgument()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), inquiry_command_complete_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 6> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(inquiry_command_complete_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| intercepted = std::nullopt; |
| result = Status::Cancelled(); // Unregister the interceptor. |
| // Try to register an interceptor for Command Status event. |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| emboss::EventCode::COMMAND_STATUS, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| |
| // Should fail, not allowed. |
| EXPECT_FALSE(result.ok()); |
| EXPECT_EQ(result.status(), Status::InvalidArgument()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), inquiry_command_status_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 7> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(inquiry_command_status_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| |
| result = Status::Cancelled(); // Unregister the interceptor. |
| // Register an interceptor for Command Status with a specific opcode. |
| result = test.hci_cmd_mux().RegisterEventInterceptor( |
| CommandStatusOpcode{emboss::OpCode::INQUIRY}, |
| [&](EventPacket&& packet) -> CommandMultiplexer::EventInterceptorReturn { |
| intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| ASSERT_TRUE(result.ok()); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), inquiry_command_status_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 7> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(inquiry_command_status_packet_bytes, out); |
| } |
| |
| intercepted = std::nullopt; |
| |
| // Test a different Command Status opcode (should not be intercepted). |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), disconnect_command_status_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromController(std::move(buf)); |
| |
| // Forwards packets if no interceptor. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 7> out; |
| test.packets_to_host().front()->CopyTo(out); |
| EXPECT_EQ(disconnect_command_status_packet_bytes, out); |
| test.packets_to_host().pop_front(); |
| } |
| } |
| |
| TEST_F(CommandMultiplexerTest, InterceptEventsAsync) { |
| TestInterceptEvents(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, InterceptEventsTimer) { |
| TestInterceptEvents(accessor_timer()); |
| } |
| |
| void TestQueueCommands(Accessor test) { |
| static constexpr std::array<std::byte, 4> reset_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 4> inquiry_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 7> disconnect_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Disconnect) |
| std::byte(0x06), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x03), |
| // Command handle |
| std::byte(0x00), |
| std::byte(0x00), |
| // Status code |
| std::byte(0x00), |
| }; |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Packet should have been sent. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, reset_packet_bytes.size()> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), inquiry_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| } |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), disconnect_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| } |
| |
| // Packet should have been queued and not yet sent to the controller. |
| ASSERT_EQ(test.packets_to_controller().size(), 0u); |
| |
| // Give credit, confirm the queued packet was sent. |
| test.GiveCommandCredit({.opcode = 0x0C03}); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, inquiry_packet_bytes.size()> out; |
| EXPECT_EQ(test.packets_to_controller().front()->size(), 4u); |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(inquiry_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| |
| // Give last credit, confirm the second queued packet was sent. |
| test.GiveCommandCredit({.opcode = 0x0401}); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, disconnect_packet_bytes.size()> out2; |
| EXPECT_EQ(test.packets_to_controller().front()->size(), 7u); |
| test.packets_to_controller().front()->CopyTo(out2); |
| EXPECT_EQ(disconnect_packet_bytes, out2); |
| test.packets_to_controller().pop_front(); |
| |
| // Give four credits. |
| test.GiveCommandCredit({.opcode = 0x0406, .count = 4}); |
| ASSERT_EQ(test.packets_to_controller().size(), 0u); |
| |
| // Send a couple commands, ensuring they get sent immediately. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), inquiry_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| } |
| |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 4> out3; |
| EXPECT_EQ(test.packets_to_controller().front()->size(), 4u); |
| test.packets_to_controller().front()->CopyTo(out3); |
| EXPECT_EQ(inquiry_packet_bytes, out3); |
| test.packets_to_controller().pop_front(); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), disconnect_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| } |
| |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 7> out4; |
| EXPECT_EQ(test.packets_to_controller().front()->size(), 7u); |
| test.packets_to_controller().front()->CopyTo(out4); |
| EXPECT_EQ(disconnect_packet_bytes, out4); |
| test.packets_to_controller().pop_front(); |
| |
| // Send a command credit packet with 0 credits, ensuring it is overridden. |
| test.GiveCommandCredit({.count = 0}); |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), inquiry_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| } |
| |
| // Packet should have been queued and not yet sent to the controller. |
| ASSERT_EQ(test.packets_to_controller().size(), 0u); |
| |
| // Give credit, confirm the queued packet was sent. |
| test.GiveCommandCredit({.opcode = 0x0401}); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, reset_packet_bytes.size()> out5; |
| EXPECT_EQ(test.packets_to_controller().front()->size(), 4u); |
| test.packets_to_controller().front()->CopyTo(out5); |
| EXPECT_EQ(inquiry_packet_bytes, out5); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| TEST_F(CommandMultiplexerTest, QueueCommandsAsync) { |
| TestQueueCommands(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, QueueCommandsTimer) { |
| TestQueueCommands(accessor_timer()); |
| } |
| |
| void TestNumHciCommands(Accessor test) { |
| constexpr uint8_t kTestNumHciCommands = 5; |
| constexpr size_t kNumHciCommandsByte = 3; |
| static constexpr std::array<std::byte, 4> reset_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromHost(std::move(buf)); |
| } |
| |
| // Packet should have been sent. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, reset_packet_bytes.size()> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| |
| // Give credit, confirm the queued packet was sent. |
| test.GiveCommandCredit({.opcode = 0x0C03, |
| .count = kTestNumHciCommands, |
| .remove_from_host_buffer = false}); |
| ASSERT_EQ(test.packets_to_controller().size(), 0u); |
| |
| // Check that the num_hci_commands field of the command complete packet is >= |
| // the value sent from. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, kCommandCompleteHeaderSize> command_complete_header; |
| test.packets_to_host().front()->CopyTo(command_complete_header); |
| uint8_t actual_num_hci_commands = |
| static_cast<uint8_t>(command_complete_header[kNumHciCommandsByte]); |
| EXPECT_GE(actual_num_hci_commands, kTestNumHciCommands); |
| test.packets_to_host().pop_front(); |
| |
| for (size_t i = 0; i < kTestNumHciCommands; ++i) { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromHost(std::move(buf)); |
| ASSERT_EQ(test.packets_to_controller().size(), 1); |
| std::array<std::byte, reset_packet_bytes.size()> out_packet; |
| test.packets_to_controller().front()->CopyTo(out_packet); |
| EXPECT_EQ(reset_packet_bytes, out_packet); |
| test.packets_to_controller().clear(); |
| } |
| |
| if (actual_num_hci_commands > kTestNumHciCommands) { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.hci_cmd_mux().HandleH4FromHost(std::move(buf)); |
| // All further packets should be buffered. |
| EXPECT_EQ(test.packets_to_controller().size(), 0u); |
| } |
| } |
| |
| TEST_F(CommandMultiplexerTest, NumHciCommandsAsync) { |
| TestNumHciCommands(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, NumHciCommandsTimer) { |
| TestNumHciCommands(accessor_timer()); |
| } |
| |
| void TestDeadlock(Accessor test) { |
| static constexpr std::array<std::byte, 5> command_complete_packet_bytes{ |
| // Event code (Command Complete) |
| std::byte(0x0E), |
| // Parameter size |
| std::byte(0x03), |
| // Num_HCI_Command_Packets |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| }; |
| |
| // Used to make sure the callback only takes one local reference |
| struct CallbackData { |
| MultiBuf::Instance event_buf; |
| Accessor& test; |
| std::optional<MultiBuf::Instance> intercepted; |
| }; |
| |
| CallbackData callback_data{ |
| .event_buf = MultiBuf::Instance(test.allocator()), |
| .test = test, |
| .intercepted = std::nullopt, |
| }; |
| auto& intercepted = callback_data.intercepted; |
| |
| callback_data.event_buf->Insert(callback_data.event_buf->end(), |
| command_complete_packet_bytes); |
| |
| auto result = test.hci_cmd_mux().RegisterCommandInterceptor( |
| emboss::OpCode::RESET, |
| [&callback_data](CommandPacket&& packet) |
| -> CommandMultiplexer::CommandInterceptorReturn { |
| // This will acquire `mutex_` to send an event. |
| EXPECT_TRUE(callback_data.test.hci_cmd_mux() |
| .SendEvent({std::move(callback_data.event_buf)}) |
| .has_value()); |
| callback_data.intercepted = std::move(packet.buffer); |
| return {}; |
| }); |
| ASSERT_EQ(result.status(), OkStatus()); |
| |
| static constexpr std::array<std::byte, 4> reset_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| |
| // Ensure not forwarded. |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| ASSERT_TRUE(intercepted.has_value()); |
| std::array<std::byte, 4> out; |
| intercepted.value()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| |
| // Ensure event was sent. |
| ASSERT_EQ(test.packets_to_host().size(), 1u); |
| std::array<std::byte, 1> h4_header{}; |
| test.packets_to_host().front()->CopyTo(h4_header); |
| EXPECT_EQ(h4_header[0], std::byte(emboss::H4PacketType::EVENT)); |
| std::array<std::byte, command_complete_packet_bytes.size()> event_payload{}; |
| test.packets_to_host().front()->CopyTo(event_payload, 1); |
| EXPECT_EQ(command_complete_packet_bytes, event_payload); |
| test.packets_to_host().pop_front(); |
| } |
| |
| TEST_F(CommandMultiplexerTest, DeadlockAsync) { |
| TestDeadlock(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, DeadlockTimer) { |
| TestDeadlock(accessor_timer()); |
| } |
| |
| void TestSendCommandThenHostCommandIntermingle(Accessor test) { |
| static constexpr std::array<std::byte, 4> reset_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 3> |
| read_local_version_information_packet_bytes{ |
| // OpCode (Read Local Version Information) |
| std::byte(0x01), |
| std::byte(0x10), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 4> inquiry_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| // Send the first command to use up the initial command credit. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| } |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, reset_packet_bytes.size()> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| |
| // Send a command via SendCommand. This should be queued. |
| MultiBuf::Instance send_cmd_buf(test.allocator()); |
| send_cmd_buf->Insert(send_cmd_buf->end(), |
| read_local_version_information_packet_bytes); |
| bool send_cmd_event_handler_called = false; |
| auto send_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(send_cmd_buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| send_cmd_event_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE); |
| EXPECT_TRUE(send_cmd_result.has_value()); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| |
| // Send a command via HandleH4FromHost. This should also be queued. |
| MultiBuf::Instance host_cmd_buf(test.allocator()); |
| host_cmd_buf->Insert(host_cmd_buf->end(), inquiry_packet_bytes); |
| test.SendFromHost(std::move(host_cmd_buf)); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| |
| // Give credit. The command from SendCommand should be sent first. |
| test.GiveCommandCredit(); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> send_cmd_h4_header{}; |
| std::array<std::byte, read_local_version_information_packet_bytes.size()> |
| send_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(send_cmd_h4_header); |
| test.packets_to_controller().front()->CopyTo(send_cmd_out, 1); |
| EXPECT_EQ(send_cmd_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(read_local_version_information_packet_bytes, send_cmd_out); |
| test.packets_to_controller().pop_front(); |
| |
| // Give another credit. The command from HandleH4FromHost should be sent. |
| test.GiveCommandCredit(); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, inquiry_packet_bytes.size()> host_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(host_cmd_out); |
| EXPECT_EQ(inquiry_packet_bytes, host_cmd_out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| TEST_F(CommandMultiplexerTest, SendCommandThenHostCommandIntermingleAsync) { |
| TestSendCommandThenHostCommandIntermingle(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, SendCommandThenHostCommandIntermingleTimer) { |
| TestSendCommandThenHostCommandIntermingle(accessor_timer()); |
| } |
| |
| void TestHostCommandThenSendCommandIntermingle(Accessor test) { |
| static constexpr std::array<std::byte, 4> reset_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Reset) |
| std::byte(0x03), |
| std::byte(0x0C), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 3> |
| read_local_version_information_packet_bytes{ |
| // OpCode (Read Local Version Information) |
| std::byte(0x01), |
| std::byte(0x10), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 4> inquiry_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| // Send the first command to use up the initial command credit. |
| { |
| MultiBuf::Instance buf(test.allocator()); |
| buf->Insert(buf->end(), reset_packet_bytes); |
| test.SendFromHost(std::move(buf)); |
| } |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, reset_packet_bytes.size()> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(reset_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| |
| // Send a command via HandleH4FromHost. This should be queued. |
| MultiBuf::Instance host_cmd_buf(test.allocator()); |
| host_cmd_buf->Insert(host_cmd_buf->end(), inquiry_packet_bytes); |
| test.SendFromHost(std::move(host_cmd_buf)); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| |
| // Send a command via SendCommand. This should also be queued. |
| MultiBuf::Instance send_cmd_buf(test.allocator()); |
| send_cmd_buf->Insert(send_cmd_buf->end(), |
| read_local_version_information_packet_bytes); |
| bool send_cmd_event_handler_called = false; |
| auto send_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(send_cmd_buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| send_cmd_event_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE); |
| EXPECT_TRUE(send_cmd_result.has_value()); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| |
| // Give credit. The command from HandleH4FromHost should be sent first. |
| test.GiveCommandCredit(); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, inquiry_packet_bytes.size()> host_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(host_cmd_out); |
| EXPECT_EQ(inquiry_packet_bytes, host_cmd_out); |
| test.packets_to_controller().pop_front(); |
| |
| // Give another credit. The command from SendCommand should be sent. |
| test.GiveCommandCredit(); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> send_cmd_h4_header{}; |
| std::array<std::byte, read_local_version_information_packet_bytes.size()> |
| send_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(send_cmd_h4_header); |
| test.packets_to_controller().front()->CopyTo(send_cmd_out, 1); |
| EXPECT_EQ(send_cmd_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(read_local_version_information_packet_bytes, send_cmd_out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| TEST_F(CommandMultiplexerTest, HostCommandThenSendCommandIntermingleAsync) { |
| TestHostCommandThenSendCommandIntermingle(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, HostCommandThenSendCommandIntermingleTimer) { |
| TestHostCommandThenSendCommandIntermingle(accessor_timer()); |
| } |
| |
| void TestSendCommandExclusions(Accessor test) { |
| static constexpr std::array<std::byte, 3> |
| read_local_version_information_packet_bytes{ |
| // OpCode (Read Local Version Information) |
| std::byte(0x01), |
| std::byte(0x10), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 3> read_bd_addr_packet_bytes{ |
| // OpCode (Read BD_ADDR) |
| std::byte(0x09), |
| std::byte(0x10), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 3> inquiry_packet_bytes{ |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| // Send a command with no exclusions. This should be sent immediately. |
| bool read_local_version_information_handler_called = false; |
| { |
| MultiBuf::Instance send_cmd_buf(test.allocator()); |
| send_cmd_buf->Insert(send_cmd_buf->end(), |
| read_local_version_information_packet_bytes); |
| auto send_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(send_cmd_buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| read_local_version_information_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE); |
| EXPECT_TRUE(send_cmd_result.has_value()); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> send_cmd_h4_header{}; |
| std::array<std::byte, read_local_version_information_packet_bytes.size()> |
| send_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(send_cmd_h4_header); |
| test.packets_to_controller().front()->CopyTo(send_cmd_out, 1); |
| EXPECT_EQ(send_cmd_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(read_local_version_information_packet_bytes, send_cmd_out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| EXPECT_FALSE(read_local_version_information_handler_called); |
| test.GiveCommandCredit(); |
| |
| // Send a command with an exclusion set that includes the previous command. |
| // This should be queued. |
| bool read_bd_addr_handler_called = false; |
| { |
| static constexpr std::array<emboss::OpCode, 1> exclusions{ |
| emboss::OpCode::READ_LOCAL_VERSION_INFO}; |
| MultiBuf::Instance send_cmd_buf(test.allocator()); |
| send_cmd_buf->Insert(send_cmd_buf->end(), read_bd_addr_packet_bytes); |
| auto send_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(send_cmd_buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| read_bd_addr_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE, |
| exclusions); |
| EXPECT_TRUE(send_cmd_result.has_value()); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| } |
| |
| // Send command complete for first command, should see second command sent. |
| { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto command_complete, |
| test.AllocBuf(MakeCommandCompletePacket(0x1001, 1))); |
| test.hci_cmd_mux().HandleH4FromController(std::move(command_complete)); |
| EXPECT_TRUE(read_local_version_information_handler_called); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> send_cmd_h4_header{}; |
| std::array<std::byte, read_bd_addr_packet_bytes.size()> send_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(send_cmd_h4_header); |
| test.packets_to_controller().front()->CopyTo(send_cmd_out, 1); |
| EXPECT_EQ(send_cmd_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(read_bd_addr_packet_bytes, send_cmd_out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| EXPECT_FALSE(read_bd_addr_handler_called); |
| test.GiveCommandCredit(); |
| |
| // Send a command with an exclusion set that includes the first command again, |
| // ensuring that this is sent immediately since that command is complete. |
| bool inquiry_handler_called = false; |
| { |
| static constexpr std::array<emboss::OpCode, 1> exclusions{ |
| emboss::OpCode::RESET}; |
| MultiBuf::Instance send_cmd_buf(test.allocator()); |
| send_cmd_buf->Insert(send_cmd_buf->end(), inquiry_packet_bytes); |
| auto send_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(send_cmd_buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| inquiry_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE, |
| exclusions); |
| EXPECT_TRUE(send_cmd_result.has_value()); |
| |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> send_cmd_h4_header{}; |
| std::array<std::byte, inquiry_packet_bytes.size()> send_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(send_cmd_h4_header); |
| test.packets_to_controller().front()->CopyTo(send_cmd_out, 1); |
| EXPECT_EQ(send_cmd_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(inquiry_packet_bytes, send_cmd_out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| EXPECT_FALSE(read_bd_addr_handler_called); |
| EXPECT_FALSE(inquiry_handler_called); |
| test.GiveCommandCredit(); |
| |
| // Send another command excluding both active commands. |
| read_local_version_information_handler_called = false; |
| { |
| static constexpr std::array<emboss::OpCode, 2> exclusions{ |
| emboss::OpCode::READ_BD_ADDR, |
| emboss::OpCode::INQUIRY, |
| }; |
| MultiBuf::Instance send_cmd_buf(test.allocator()); |
| send_cmd_buf->Insert(send_cmd_buf->end(), |
| read_local_version_information_packet_bytes); |
| auto send_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(send_cmd_buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| read_local_version_information_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE, |
| exclusions); |
| EXPECT_TRUE(send_cmd_result.has_value()); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| } |
| |
| EXPECT_FALSE(read_bd_addr_handler_called); |
| EXPECT_FALSE(inquiry_handler_called); |
| EXPECT_FALSE(read_local_version_information_handler_called); |
| |
| // Send a command complete for inquiry. |
| { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto command_complete, |
| test.AllocBuf(MakeCommandCompletePacket(0x0401, 1))); |
| test.hci_cmd_mux().HandleH4FromController(std::move(command_complete)); |
| EXPECT_TRUE(inquiry_handler_called); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| } |
| |
| EXPECT_FALSE(read_bd_addr_handler_called); |
| EXPECT_FALSE(read_local_version_information_handler_called); |
| |
| // Send a command complete for read_bd_addr, expect the last command to be |
| // sent. |
| { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto command_complete, |
| test.AllocBuf(MakeCommandCompletePacket(0x1009, 1))); |
| test.hci_cmd_mux().HandleH4FromController(std::move(command_complete)); |
| EXPECT_TRUE(read_bd_addr_handler_called); |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 1> send_cmd_h4_header{}; |
| std::array<std::byte, read_local_version_information_packet_bytes.size()> |
| send_cmd_out{}; |
| test.packets_to_controller().front()->CopyTo(send_cmd_h4_header); |
| test.packets_to_controller().front()->CopyTo(send_cmd_out, 1); |
| EXPECT_EQ(send_cmd_h4_header[0], std::byte(emboss::H4PacketType::COMMAND)); |
| EXPECT_EQ(read_local_version_information_packet_bytes, send_cmd_out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| EXPECT_FALSE(read_local_version_information_handler_called); |
| |
| // Send the last command complete |
| { |
| PW_TEST_ASSERT_OK_AND_ASSIGN( |
| auto command_complete, |
| test.AllocBuf(MakeCommandCompletePacket(0x1001, 1))); |
| test.hci_cmd_mux().HandleH4FromController(std::move(command_complete)); |
| EXPECT_TRUE(read_local_version_information_handler_called); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| } |
| } |
| |
| TEST_F(CommandMultiplexerTest, SendCommandExclusionsAsync) { |
| TestSendCommandExclusions(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, SendCommandExclusionsTimer) { |
| TestSendCommandExclusions(accessor_timer()); |
| } |
| |
| void TestCreditRecoveredWhenIntercepted(Accessor test) { |
| static constexpr std::array<std::byte, 4> inquiry_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| // Consume initial credit. |
| test.GiveCommandCredit({.count = 0}); |
| |
| // Host sends command 1. It gets queued since credits = 0. |
| MultiBuf::Instance buf1(test.allocator()); |
| buf1->Insert(buf1->end(), inquiry_packet_bytes); |
| test.SendFromHost(std::move(buf1)); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| |
| // Register an interceptor that swallows Command Complete events. |
| auto interceptor = test.hci_cmd_mux().RegisterEventInterceptor( |
| CommandCompleteOpcode{emboss::OpCode::INQUIRY}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| return {}; // Do not return the event, so it is swallowed. |
| }); |
| ASSERT_TRUE(interceptor.ok()); |
| |
| // Controller sends Command Complete with 1 credit. |
| auto command_complete_result = |
| test.AllocBuf(MakeCommandCompletePacket(0x0401, 1)); |
| ASSERT_TRUE(command_complete_result.ok()); |
| test.hci_cmd_mux().HandleH4FromController( |
| std::move(*command_complete_result)); |
| |
| // The event should have been swallowed. |
| EXPECT_TRUE(test.packets_to_host().empty()); |
| |
| // However, the queued command should have been sent to the controller. |
| ASSERT_EQ(test.packets_to_controller().size(), 1u); |
| std::array<std::byte, 4> out; |
| test.packets_to_controller().front()->CopyTo(out); |
| EXPECT_EQ(inquiry_packet_bytes, out); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| TEST_F(CommandMultiplexerTest, CreditRecoveredWhenInterceptedAsync) { |
| TestCreditRecoveredWhenIntercepted(accessor_async2()); |
| } |
| TEST_F(CommandMultiplexerTest, CreditRecoveredWhenInterceptedTimer) { |
| TestCreditRecoveredWhenIntercepted(accessor_timer()); |
| } |
| |
| void TestReset(Accessor test) { |
| static constexpr std::array<std::byte, 3> |
| read_local_version_information_packet_bytes{ |
| // OpCode (Read Local Version Information) |
| std::byte(0x01), |
| std::byte(0x10), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| static constexpr std::array<std::byte, 4> inquiry_packet_bytes{ |
| // Packet type (command) |
| std::byte(0x01), |
| // OpCode (Inquiry) |
| std::byte(0x01), |
| std::byte(0x04), |
| // Parameter size |
| std::byte(0x00), |
| }; |
| |
| // Consume initial credit |
| test.GiveCommandCredit({.count = 0}); |
| |
| // Queue a command via SendCommand |
| MultiBuf::Instance send_cmd_buf(test.allocator()); |
| send_cmd_buf->Insert(send_cmd_buf->end(), |
| read_local_version_information_packet_bytes); |
| auto send_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(send_cmd_buf)}, |
| [](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE); |
| EXPECT_TRUE(send_cmd_result.has_value()); |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| |
| // Call Reset() |
| test.hci_cmd_mux().Reset(); |
| |
| // The queue should have been cleared, so the queued command was never sent. |
| // We can verify this since packets_to_controller is now empty. |
| EXPECT_TRUE(test.packets_to_controller().empty()); |
| |
| // Test that command_credits_ are reset to 1 by sending a command after Reset, |
| // which should be forwarded to the controller immediately. |
| MultiBuf::Instance host_cmd_buf(test.allocator()); |
| host_cmd_buf->Insert(host_cmd_buf->end(), inquiry_packet_bytes); |
| test.SendFromHost(std::move(host_cmd_buf)); |
| EXPECT_EQ(test.packets_to_controller().size(), 1u); |
| test.packets_to_controller().pop_front(); |
| |
| // Give a credit so the next injected command can be sent immediately. |
| test.GiveCommandCredit({.opcode = 0x0401}); |
| |
| // Test that active_command_queue_ is cleared by sending a command that is |
| // immediately sent (active) |
| bool event_handler_called = false; |
| MultiBuf::Instance active_cmd_buf(test.allocator()); |
| active_cmd_buf->Insert(active_cmd_buf->end(), |
| read_local_version_information_packet_bytes); |
| auto active_cmd_result = test.hci_cmd_mux().SendCommand( |
| {std::move(active_cmd_buf)}, |
| [&](EventPacket&&) -> CommandMultiplexer::EventInterceptorReturn { |
| event_handler_called = true; |
| return {}; |
| }, |
| emboss::EventCode::COMMAND_COMPLETE); |
| EXPECT_TRUE(active_cmd_result.has_value()); |
| EXPECT_EQ(test.packets_to_controller().size(), 1u); |
| test.packets_to_controller().pop_front(); |
| } |
| |
| TEST_F(CommandMultiplexerTest, ResetAsync) { TestReset(accessor_async2()); } |
| TEST_F(CommandMultiplexerTest, ResetTimer) { TestReset(accessor_timer()); } |
| |
| } // namespace |
| } // namespace pw::bluetooth::proxy::hci |