| // 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_sapphire/central.h" |
| |
| #include <array> |
| |
| #include "pw_async/fake_dispatcher.h" |
| #include "pw_async2/pend_func_task.h" |
| #include "pw_async2/poll.h" |
| #include "pw_bluetooth/uuid.h" |
| #include "pw_bluetooth_sapphire/internal/discovery_filter.h" |
| #include "pw_bluetooth_sapphire/internal/host/gap/fake_adapter.h" |
| #include "pw_bluetooth_sapphire/internal/host/hci/discovery_filter.h" |
| #include "pw_bluetooth_sapphire/internal/uuid.h" |
| #include "pw_multibuf/simple_allocator_for_test.h" |
| #include "pw_unit_test/framework.h" |
| |
| namespace { |
| |
| using pw::bluetooth_sapphire::Central; |
| using ScanStartResult = Central::ScanStartResult; |
| using Pending = pw::async2::PendingType; |
| using Ready = pw::async2::ReadyType; |
| using Context = pw::async2::Context; |
| using ScanHandle = Central::ScanHandle; |
| using ScanResult = Central::ScanResult; |
| using pw::async2::PendFuncTask; |
| using pw::async2::Poll; |
| using pw::bluetooth_sapphire::internal::UuidFrom; |
| using pw::chrono::SystemClock; |
| using ScanFilter = Central::ScanFilter; |
| using DisconnectReason = |
| pw::bluetooth::low_energy::Connection2::DisconnectReason; |
| |
| const bt::DeviceAddress kAddress0(bt::DeviceAddress::Type::kLEPublic, {0}); |
| const bt::StaticByteBuffer kAdvDataWithName(0x05, // length |
| 0x09, // type (name) |
| 'T', |
| 'e', |
| 's', |
| 't'); |
| |
| auto MakePendResultTask( |
| ScanHandle::Ptr& scan_handle, |
| std::optional<pw::Result<ScanResult>>& scan_result_out) { |
| return PendFuncTask([&scan_handle, &scan_result_out](Context& cx) -> Poll<> { |
| Poll<pw::Result<ScanResult>> pend = scan_handle->PendResult(cx); |
| if (pend.IsPending()) { |
| return Pending(); |
| } |
| scan_result_out = std::move(pend.value()); |
| return Ready(); |
| }); |
| } |
| |
| class CentralTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| central_.emplace( |
| adapter_.AsWeakPtr(), async_dispatcher_, multibuf_allocator_); |
| } |
| |
| ScanHandle::Ptr Scan(Central::ScanOptions& options) { |
| pw::async2::OnceReceiver<ScanStartResult> scan_receiver = |
| central().Scan(options); |
| |
| std::optional<pw::Result<ScanStartResult>> scan_pend_result; |
| PendFuncTask scan_receiver_task( |
| [&scan_receiver, &scan_pend_result](Context& cx) -> Poll<> { |
| Poll<pw::Result<ScanStartResult>> scan_pend = scan_receiver.Pend(cx); |
| if (scan_pend.IsPending()) { |
| return Pending(); |
| } |
| scan_pend_result = std::move(scan_pend.value()); |
| return Ready(); |
| }); |
| async2_dispatcher().Post(scan_receiver_task); |
| EXPECT_FALSE(scan_pend_result.has_value()); |
| |
| async_dispatcher().RunUntilIdle(); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| |
| if (!scan_pend_result.has_value()) { |
| ADD_FAILURE(); |
| return nullptr; |
| } |
| if (!scan_pend_result.value().ok()) { |
| ADD_FAILURE(); |
| return nullptr; |
| } |
| ScanStartResult scan_start_result = |
| std::move(scan_pend_result.value().value()); |
| if (!scan_start_result.has_value()) { |
| ADD_FAILURE(); |
| return nullptr; |
| } |
| return std::move(scan_start_result.value()); |
| } |
| |
| void DestroyCentral() { central_.reset(); } |
| |
| auto connections() { return adapter().fake_le()->connections(); } |
| |
| bt::gap::testing::FakeAdapter& adapter() { return adapter_; } |
| bt::gap::PeerCache& peer_cache() { return *adapter_.peer_cache(); } |
| pw::bluetooth_sapphire::Central& central() { return central_.value(); } |
| pw::async::test::FakeDispatcher& async_dispatcher() { |
| return async_dispatcher_; |
| } |
| pw::async2::Dispatcher& async2_dispatcher() { return async2_dispatcher_; } |
| |
| private: |
| pw::async::test::FakeDispatcher async_dispatcher_; |
| pw::async2::Dispatcher async2_dispatcher_; |
| bt::gap::testing::FakeAdapter adapter_{async_dispatcher_}; |
| |
| pw::multibuf::test::SimpleAllocatorForTest</*kDataSizeBytes=*/2024, |
| /*kMetaSizeBytes=*/3000> |
| multibuf_allocator_; |
| std::optional<Central> central_; |
| }; |
| |
| TEST_F(CentralTest, ScanOneResultAndStopScanSuccess) { |
| Central::ScanOptions options; |
| options.scan_type = Central::ScanType::kActiveUsePublicAddress; |
| // Don't filter results. |
| std::array<Central::ScanFilter, 1> filters{Central::ScanFilter{}}; |
| options.filters = filters; |
| |
| ScanHandle::Ptr scan_handle = Scan(options); |
| ASSERT_TRUE(scan_handle); |
| ASSERT_EQ(adapter().fake_le()->discovery_sessions().size(), 1u); |
| EXPECT_TRUE((*adapter().fake_le()->discovery_sessions().cbegin())->active()); |
| |
| std::optional<pw::Result<ScanResult>> scan_result_result; |
| PendFuncTask scan_handle_task = |
| MakePendResultTask(scan_handle, scan_result_result); |
| async2_dispatcher().Post(scan_handle_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| |
| const bool connectable = true; |
| bt::gap::Peer* peer = peer_cache().NewPeer(kAddress0, connectable); |
| const int rssi = 5; |
| SystemClock::time_point timestamp(SystemClock::duration(5)); |
| peer->MutLe().SetAdvertisingData(rssi, kAdvDataWithName, timestamp); |
| |
| adapter().fake_le()->NotifyScanResult(*peer); |
| |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| ASSERT_TRUE(scan_result_result.has_value()); |
| ASSERT_TRUE(scan_result_result.value().ok()); |
| |
| ScanResult scan_result = std::move(scan_result_result.value().value()); |
| scan_result_result.reset(); |
| EXPECT_EQ(scan_result.peer_id, peer->identifier().value()); |
| EXPECT_EQ(scan_result.connectable, connectable); |
| EXPECT_EQ(scan_result.rssi, rssi); |
| EXPECT_EQ(scan_result.last_updated, timestamp); |
| ASSERT_EQ(scan_result.data.size(), kAdvDataWithName.size()); |
| ASSERT_TRUE(scan_result.data.IsContiguous()); |
| for (size_t i = 0; i < kAdvDataWithName.size(); i++) { |
| EXPECT_EQ(scan_result.data.ContiguousSpan().value()[i], |
| kAdvDataWithName.subspan()[i]); |
| } |
| ASSERT_TRUE(scan_result.name.has_value()); |
| EXPECT_EQ(scan_result.name.value(), "Test"); |
| |
| // No more scan results should be received. |
| async2_dispatcher().Post(scan_handle_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| EXPECT_FALSE(scan_result_result.has_value()); |
| scan_handle_task.Deregister(); |
| |
| // Stop scan |
| scan_handle.reset(); |
| // The scan should stop asynchronously. |
| EXPECT_EQ(adapter().fake_le()->discovery_sessions().size(), 1u); |
| async_dispatcher().RunUntilIdle(); |
| EXPECT_EQ(adapter().fake_le()->discovery_sessions().size(), 0u); |
| } |
| |
| TEST_F(CentralTest, DiscoveryFilterFrom) { |
| ScanFilter scan_filter; |
| scan_filter.service_uuid = pw::bluetooth::Uuid(1); |
| scan_filter.service_data_uuid = pw::bluetooth::Uuid(2); |
| scan_filter.manufacturer_id = 3; |
| scan_filter.connectable = true; |
| scan_filter.name = "bluetooth"; |
| scan_filter.max_path_loss = 4; |
| scan_filter.solicitation_uuid = pw::bluetooth::Uuid(6); |
| |
| bt::hci::DiscoveryFilter discovery_filter = |
| pw::bluetooth_sapphire::internal::DiscoveryFilterFrom(scan_filter); |
| |
| EXPECT_EQ(1u, discovery_filter.service_uuids().size()); |
| EXPECT_EQ(UuidFrom(scan_filter.service_uuid.value()), |
| discovery_filter.service_uuids()[0]); |
| |
| EXPECT_EQ(1u, discovery_filter.service_data_uuids().size()); |
| EXPECT_EQ(UuidFrom(scan_filter.service_data_uuid.value()), |
| discovery_filter.service_data_uuids()[0]); |
| |
| EXPECT_EQ(1u, discovery_filter.solicitation_uuids().size()); |
| EXPECT_EQ(UuidFrom(scan_filter.solicitation_uuid.value()), |
| discovery_filter.solicitation_uuids()[0]); |
| |
| EXPECT_EQ(scan_filter.manufacturer_id, discovery_filter.manufacturer_code()); |
| EXPECT_EQ(scan_filter.connectable, discovery_filter.connectable()); |
| EXPECT_EQ(scan_filter.name, discovery_filter.name_substring()); |
| EXPECT_EQ(scan_filter.max_path_loss, discovery_filter.pathloss()); |
| } |
| |
| TEST_F(CentralTest, ScanErrorReceivedByScanHandle) { |
| Central::ScanOptions options; |
| options.scan_type = Central::ScanType::kActiveUsePublicAddress; |
| // Don't filter results. |
| std::array<Central::ScanFilter, 1> filters{Central::ScanFilter{}}; |
| options.filters = filters; |
| |
| ScanHandle::Ptr scan_handle = Scan(options); |
| ASSERT_TRUE(scan_handle); |
| ASSERT_EQ(adapter().fake_le()->discovery_sessions().size(), 1u); |
| EXPECT_TRUE((*adapter().fake_le()->discovery_sessions().cbegin())->active()); |
| |
| std::optional<pw::Result<ScanResult>> scan_result_result; |
| PendFuncTask scan_handle_task = |
| MakePendResultTask(scan_handle, scan_result_result); |
| async2_dispatcher().Post(scan_handle_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| |
| (*adapter().fake_le()->discovery_sessions().cbegin())->NotifyError(); |
| |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| ASSERT_TRUE(scan_result_result.has_value()); |
| EXPECT_TRUE(scan_result_result.value().status().IsCancelled()); |
| } |
| |
| TEST_F(CentralTest, ScanWithoutFiltersFails) { |
| Central::ScanOptions options; |
| options.scan_type = Central::ScanType::kActiveUsePublicAddress; |
| options.filters = {}; |
| |
| pw::async2::OnceReceiver<ScanStartResult> scan_receiver = |
| central().Scan(options); |
| |
| std::optional<pw::Result<ScanStartResult>> scan_pend_result; |
| PendFuncTask scan_receiver_task( |
| [&scan_receiver, &scan_pend_result](Context& cx) -> Poll<> { |
| Poll<pw::Result<ScanStartResult>> scan_pend = scan_receiver.Pend(cx); |
| if (scan_pend.IsPending()) { |
| return Pending(); |
| } |
| scan_pend_result = std::move(scan_pend.value()); |
| return Ready(); |
| }); |
| async2_dispatcher().Post(scan_receiver_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| ASSERT_TRUE(scan_pend_result.has_value()); |
| ASSERT_TRUE(scan_pend_result.value().ok()); |
| ScanStartResult scan_start_result = |
| std::move(scan_pend_result.value().value()); |
| ASSERT_FALSE(scan_start_result.has_value()); |
| EXPECT_EQ(scan_start_result.error(), |
| Central::StartScanError::kInvalidParameters); |
| } |
| |
| TEST_F(CentralTest, QueueMoreThanMaxScanResultsInScanHandleDropsOldest) { |
| Central::ScanOptions options; |
| options.scan_type = Central::ScanType::kActiveUsePublicAddress; |
| // Don't filter results. |
| std::array<Central::ScanFilter, 1> filters{Central::ScanFilter{}}; |
| options.filters = filters; |
| |
| ScanHandle::Ptr scan_handle = Scan(options); |
| ASSERT_TRUE(scan_handle); |
| ASSERT_EQ(adapter().fake_le()->discovery_sessions().size(), 1u); |
| EXPECT_TRUE((*adapter().fake_le()->discovery_sessions().cbegin())->active()); |
| |
| std::vector<pw::Result<ScanResult>> scan_result_results; |
| PendFuncTask scan_handle_task = |
| PendFuncTask([&scan_handle, &scan_result_results](Context& cx) -> Poll<> { |
| while (true) { |
| Poll<pw::Result<ScanResult>> pend = scan_handle->PendResult(cx); |
| if (pend.IsPending()) { |
| return Pending(); |
| } |
| scan_result_results.emplace_back(std::move(pend.value())); |
| if (!scan_result_results.back().ok()) { |
| return Ready(); |
| } |
| } |
| }); |
| async2_dispatcher().Post(scan_handle_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| |
| const bool connectable = true; |
| bt::gap::Peer* peer = peer_cache().NewPeer(kAddress0, connectable); |
| SystemClock::time_point timestamp(SystemClock::duration(5)); |
| |
| // Queue 1 more than the max queue size. Put the index in the rssi field. |
| for (int i = 0; i < Central::kMaxScanResultsQueueSize + 1; i++) { |
| peer->MutLe().SetAdvertisingData(/*rssi=*/i, kAdvDataWithName, timestamp); |
| adapter().fake_le()->NotifyScanResult(*peer); |
| } |
| |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| scan_handle_task.Deregister(); |
| ASSERT_EQ(scan_result_results.size(), Central::kMaxScanResultsQueueSize); |
| // The first scan result should have been dropped. |
| for (int i = 0; i < Central::kMaxScanResultsQueueSize; i++) { |
| ASSERT_TRUE(scan_result_results[i].ok()); |
| EXPECT_EQ(scan_result_results[i].value().rssi, i + 1); |
| } |
| } |
| |
| TEST_F(CentralTest, CentralDestroyedBeforeScanHandle) { |
| Central::ScanOptions options; |
| options.scan_type = Central::ScanType::kActiveUsePublicAddress; |
| std::array<Central::ScanFilter, 1> filters{Central::ScanFilter{}}; |
| options.filters = filters; |
| |
| ScanHandle::Ptr scan_handle = Scan(options); |
| ASSERT_TRUE(scan_handle); |
| ASSERT_EQ(adapter().fake_le()->discovery_sessions().size(), 1u); |
| |
| std::optional<pw::Result<ScanResult>> scan_result_result; |
| PendFuncTask scan_handle_task = |
| MakePendResultTask(scan_handle, scan_result_result); |
| async2_dispatcher().Post(scan_handle_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| |
| DestroyCentral(); |
| |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| ASSERT_TRUE(scan_result_result.has_value()); |
| EXPECT_TRUE(scan_result_result.value().status().IsCancelled()); |
| |
| scan_handle.reset(); |
| } |
| |
| TEST_F(CentralTest, ConnectAndDisconnectSuccess) { |
| bt::gap::Peer* peer = peer_cache().NewPeer(kAddress0, /*connectable=*/true); |
| pw::bluetooth::low_energy::Connection2::ConnectionOptions options; |
| std::optional<pw::Result<Central::ConnectResult>> connect_result; |
| pw::async2::OnceReceiver<Central::ConnectResult> receiver = |
| central().Connect(peer->identifier().value(), options); |
| PendFuncTask connect_task = |
| PendFuncTask([&connect_result, &receiver](Context& cx) -> Poll<> { |
| Poll<pw::Result<Central::ConnectResult>> poll = receiver.Pend(cx); |
| if (poll.IsPending()) { |
| return Pending(); |
| } |
| connect_result = std::move(poll->value()); |
| return Ready(); |
| }); |
| async2_dispatcher().Post(connect_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| async_dispatcher().RunUntilIdle(); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| ASSERT_TRUE(connect_result.has_value()); |
| ASSERT_TRUE(connect_result->ok()); |
| ASSERT_TRUE(connect_result->value()); |
| ASSERT_EQ(adapter().fake_le()->connections().count(peer->identifier()), 1u); |
| pw::bluetooth::low_energy::Connection2::Ptr connection = |
| std::move(connect_result->value().value()); |
| |
| // Disconnect |
| connection.reset(); |
| ASSERT_EQ(connections().count(peer->identifier()), 1u); |
| async_dispatcher().RunUntilIdle(); |
| ASSERT_EQ(connections().count(peer->identifier()), 0u); |
| } |
| |
| TEST_F(CentralTest, PendDisconnect) { |
| bt::gap::Peer* peer = peer_cache().NewPeer(kAddress0, /*connectable=*/true); |
| pw::bluetooth::low_energy::Connection2::ConnectionOptions options; |
| std::optional<pw::Result<Central::ConnectResult>> connect_result; |
| pw::async2::OnceReceiver<Central::ConnectResult> receiver = |
| central().Connect(peer->identifier().value(), options); |
| PendFuncTask connect_task = |
| PendFuncTask([&connect_result, &receiver](Context& cx) -> Poll<> { |
| Poll<pw::Result<Central::ConnectResult>> poll = receiver.Pend(cx); |
| if (poll.IsPending()) { |
| return Pending(); |
| } |
| connect_result = std::move(poll->value()); |
| return Ready(); |
| }); |
| async2_dispatcher().Post(connect_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| async_dispatcher().RunUntilIdle(); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| ASSERT_TRUE(connect_result.has_value()); |
| ASSERT_TRUE(connect_result->ok()); |
| ASSERT_TRUE(connect_result->value()); |
| ASSERT_EQ(adapter().fake_le()->connections().count(peer->identifier()), 1u); |
| pw::bluetooth::low_energy::Connection2::Ptr connection = |
| std::move(connect_result->value().value()); |
| |
| std::optional<DisconnectReason> disconnect_reason; |
| PendFuncTask disconnect_task = |
| PendFuncTask([&connection, &disconnect_reason](Context& cx) -> Poll<> { |
| Poll<DisconnectReason> poll = connection->PendDisconnect(cx); |
| if (poll.IsPending()) { |
| return Pending(); |
| } |
| disconnect_reason = poll.value(); |
| return Ready(); |
| }); |
| async2_dispatcher().Post(disconnect_task); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsPending()); |
| ASSERT_FALSE(disconnect_reason.has_value()); |
| |
| ASSERT_TRUE(adapter().fake_le()->Disconnect(peer->identifier())); |
| ASSERT_EQ(adapter().fake_le()->connections().count(peer->identifier()), 0u); |
| EXPECT_TRUE(async2_dispatcher().RunUntilStalled().IsReady()); |
| ASSERT_TRUE(disconnect_reason.has_value()); |
| EXPECT_EQ(disconnect_reason.value(), DisconnectReason::kFailure); |
| |
| connection.reset(); |
| async_dispatcher().RunUntilIdle(); |
| } |
| |
| } // namespace |