| // Copyright 2024 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/hci/extended_low_energy_scanner.h" |
| |
| #include <unordered_map> |
| |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/hci/fake_local_address_delegate.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/controller_test.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/fake_controller.h" |
| |
| namespace bt::hci { |
| |
| using bt::testing::FakeController; |
| using TestingBase = bt::testing::FakeDispatcherControllerTest<FakeController>; |
| using LEAdvertisingState = FakeController::LEAdvertisingState; |
| |
| using pw::bluetooth::emboss::LEAdvertisingDataStatus; |
| using pw::bluetooth::emboss::LEExtendedAdvertisingReportDataView; |
| using pw::bluetooth::emboss::LEExtendedAdvertisingReportDataWriter; |
| using pw::bluetooth::emboss::LEExtendedAdvertisingReportSubeventWriter; |
| using testing::FakePeer; |
| |
| constexpr pw::chrono::SystemClock::duration kPwScanResponseTimeout = |
| std::chrono::seconds(2); |
| |
| const StaticByteBuffer kPlainAdvDataBytes('T', 'e', 's', 't'); |
| const StaticByteBuffer kPlainScanRspBytes('D', 'a', 't', 'a'); |
| |
| const DeviceAddress kPublicAddr1(DeviceAddress::Type::kLEPublic, {1}); |
| const DeviceAddress kPublicAddr2(DeviceAddress::Type::kLEPublic, {2}); |
| const DeviceAddress kPublicAddr3(DeviceAddress::Type::kLEPublic, {3}); |
| |
| class ExtendedLowEnergyScannerTest : public TestingBase, |
| public LowEnergyScanner::Delegate { |
| public: |
| ExtendedLowEnergyScannerTest() = default; |
| ~ExtendedLowEnergyScannerTest() override = default; |
| |
| protected: |
| void SetUp() override { |
| TestingBase::SetUp(); |
| |
| FakeController::Settings settings; |
| settings.ApplyExtendedLEConfig(); |
| test_device()->set_settings(settings); |
| |
| scanner_ = std::make_unique<ExtendedLowEnergyScanner>( |
| &fake_address_delegate_, transport()->GetWeakPtr(), dispatcher()); |
| scanner_->set_delegate(this); |
| |
| auto p = std::make_unique<FakePeer>(kPublicAddr1, dispatcher(), true, true); |
| p->set_use_extended_advertising_pdus(true); |
| p->set_advertising_data(kPlainAdvDataBytes); |
| p->set_scan_response(kPlainScanRspBytes); |
| peers_.push_back(std::move(p)); |
| |
| p = std::make_unique<FakePeer>(kPublicAddr2, dispatcher(), true, false); |
| p->set_use_extended_advertising_pdus(true); |
| p->set_advertising_data(kPlainAdvDataBytes); |
| peers_.push_back(std::move(p)); |
| |
| p = std::make_unique<FakePeer>(kPublicAddr3, dispatcher(), true, false); |
| p->set_use_extended_advertising_pdus(true); |
| p->set_advertising_data(kPlainAdvDataBytes); |
| peers_.push_back(std::move(p)); |
| |
| StartScan(/*active=*/true); |
| RunUntilIdle(); |
| } |
| |
| void TearDown() override { |
| scanner_ = nullptr; |
| TestingBase::TearDown(); |
| } |
| |
| void OnPeerFound(const LowEnergyScanResult& result) override { |
| if (peer_found_cb_) { |
| peer_found_cb_(result); |
| } |
| } |
| |
| using PeerFoundCallback = fit::function<void(const LowEnergyScanResult&)>; |
| void set_peer_found_callback(PeerFoundCallback cb) { |
| peer_found_cb_ = std::move(cb); |
| } |
| |
| bool StartScan(bool active, |
| pw::chrono::SystemClock::duration period = |
| LowEnergyScanner::kPeriodInfinite) { |
| LowEnergyScanner::ScanOptions options{ |
| .active = active, |
| .filter_duplicates = true, |
| .period = period, |
| .scan_response_timeout = kPwScanResponseTimeout}; |
| return scanner_->StartScan(options, [](auto status) {}); |
| } |
| |
| const std::unique_ptr<FakePeer>& peer(int i) const { return peers_[i]; } |
| |
| static constexpr size_t event_prefix_size = pw::bluetooth::emboss:: |
| LEExtendedAdvertisingReportSubevent::MinSizeInBytes(); |
| static constexpr size_t report_prefix_size = |
| pw::bluetooth::emboss::LEExtendedAdvertisingReportData::MinSizeInBytes(); |
| |
| private: |
| std::unique_ptr<ExtendedLowEnergyScanner> scanner_; |
| PeerFoundCallback peer_found_cb_; |
| std::vector<std::unique_ptr<FakePeer>> peers_; |
| FakeLocalAddressDelegate fake_address_delegate_{dispatcher()}; |
| |
| BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(ExtendedLowEnergyScannerTest); |
| }; |
| |
| // Ensure we can parse a single advertising report correctly |
| TEST_F(ExtendedLowEnergyScannerTest, ParseAdvertisingReportsSingleReport) { |
| size_t data_size = peer(1)->advertising_data().size(); |
| size_t reports_size = report_prefix_size + data_size; |
| size_t packet_size = event_prefix_size + reports_size; |
| |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(1); |
| |
| LEExtendedAdvertisingReportDataWriter report( |
| packet.reports().BackingStorage().begin(), reports_size); |
| peer(1)->FillExtendedAdvertisingReport(report, |
| peer(1)->advertising_data(), |
| /*is_fragmented=*/false, |
| /*is_scan_response=*/false); |
| |
| test_device()->SendCommandChannelPacket(event.data()); |
| |
| bool peer_found_callback_called = false; |
| set_peer_found_callback([&](const LowEnergyScanResult& result) { |
| peer_found_callback_called = true; |
| EXPECT_EQ(peer(1)->address(), result.address()); |
| EXPECT_EQ(peer(1)->advertising_data().size(), result.data().size()); |
| EXPECT_EQ(peer(1)->advertising_data(), result.data()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(peer_found_callback_called); |
| } |
| |
| // Ensure we can parse multiple extended advertising reports correctly |
| TEST_F(ExtendedLowEnergyScannerTest, ParseAdvertisingReportsMultipleReports) { |
| size_t data_size = peer(1)->advertising_data().size(); |
| size_t num_reports = 2; |
| size_t single_report_size = report_prefix_size + data_size; |
| size_t reports_size = num_reports * single_report_size; |
| size_t packet_size = event_prefix_size + reports_size; |
| |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(num_reports); |
| |
| LEExtendedAdvertisingReportDataWriter report_a( |
| packet.reports().BackingStorage().begin(), single_report_size); |
| peer(1)->FillExtendedAdvertisingReport(report_a, |
| peer(1)->advertising_data(), |
| /*is_fragmented=*/false, |
| /*is_scan_response=*/false); |
| |
| LEExtendedAdvertisingReportDataWriter report_b( |
| packet.reports().BackingStorage().begin() + single_report_size, |
| single_report_size); |
| peer(2)->FillExtendedAdvertisingReport(report_b, |
| peer(2)->advertising_data(), |
| /*is_fragmented=*/false, |
| /*is_scan_response=*/false); |
| |
| test_device()->SendCommandChannelPacket(event.data()); |
| |
| bool peer_found_callback_called = false; |
| std::unordered_map<DeviceAddress, std::unique_ptr<DynamicByteBuffer>> map; |
| |
| set_peer_found_callback([&](const LowEnergyScanResult& result) { |
| peer_found_callback_called = true; |
| map[result.address()] = |
| std::make_unique<DynamicByteBuffer>(result.data().size()); |
| result.data().Copy(&*map[result.address()]); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(peer_found_callback_called); |
| |
| EXPECT_EQ(2u, map.size()); |
| EXPECT_EQ(1u, map.count(peer(1)->address())); |
| EXPECT_EQ(*map[peer(1)->address()], peer(1)->advertising_data()); |
| |
| EXPECT_EQ(1u, map.count(peer(2)->address())); |
| EXPECT_EQ(*map[peer(2)->address()], peer(2)->advertising_data()); |
| } |
| |
| // Test that we check for enough data being present before constructing a view |
| // on top of it. This case hopefully should never happen since the |
| // Controller should always send back valid data but it's better to be |
| // careful and avoid a crash. |
| TEST_F(ExtendedLowEnergyScannerTest, ParseAdvertisingReportsNotEnoughData) { |
| size_t data_size = peer(1)->advertising_data().size(); |
| size_t reports_size = report_prefix_size + data_size; |
| size_t packet_size = event_prefix_size + reports_size; |
| |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(1); |
| |
| LEExtendedAdvertisingReportDataWriter report( |
| packet.reports().BackingStorage().begin(), reports_size); |
| peer(1)->FillExtendedAdvertisingReport(report, |
| peer(1)->advertising_data(), |
| /*is_fragmented=*/false, |
| /*is_scan_response=*/false); |
| |
| // claim we need more data than we actually provided to trigger the edge case |
| report.data_length().Write(report.data_length().Read() + 1); |
| test_device()->SendCommandChannelPacket(event.data()); |
| |
| // there wasn't enough data available so we shouldn't have parsed out any |
| // advertising reports |
| set_peer_found_callback([&](const LowEnergyScanResult& result) { FAIL(); }); |
| |
| RunUntilIdle(); |
| } |
| |
| // If a series of advertising reports claim to have more than |
| // hci_spec::kMaxLEExtendedAdvertisingDataLength, we should truncate the excess. |
| // This case hopefully should never happen since the Controller should always |
| // send back valid data but it's better to be careful and avoid a bug. |
| TEST_F(ExtendedLowEnergyScannerTest, TruncateToMax) { |
| size_t data_size = std::numeric_limits<uint8_t>::max() - report_prefix_size - |
| event_prefix_size; |
| size_t reports_size = report_prefix_size + data_size; |
| size_t packet_size = event_prefix_size + reports_size; |
| size_t num_full_reports = |
| hci_spec::kMaxLEExtendedAdvertisingDataLength / data_size; |
| |
| for (size_t i = 0; i < num_full_reports; i++) { |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(1); |
| |
| LEExtendedAdvertisingReportDataWriter report( |
| packet.reports().BackingStorage().begin(), reports_size); |
| peer(1)->FillExtendedAdvertisingReport(report, |
| peer(1)->advertising_data(), |
| /*is_fragmented=*/true, |
| /*is_scan_response=*/false); |
| report.data_length().Write(data_size); |
| |
| test_device()->SendCommandChannelPacket(event.data()); |
| } |
| |
| // the final report we send has an extra byte to trigger the edge case |
| data_size = hci_spec::kMaxLEExtendedAdvertisingDataLength % data_size + 1; |
| reports_size = report_prefix_size + data_size; |
| packet_size = event_prefix_size + reports_size; |
| |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(1); |
| |
| LEExtendedAdvertisingReportDataWriter report( |
| packet.reports().BackingStorage().begin(), reports_size); |
| peer(1)->FillExtendedAdvertisingReport(report, |
| peer(1)->advertising_data(), |
| /*is_fragmented=*/false, |
| /*is_scan_response=*/false); |
| report.data_length().Write(data_size); |
| |
| size_t result_data_length = 0; |
| set_peer_found_callback([&](const LowEnergyScanResult& result) { |
| result_data_length = result.data().size(); |
| }); |
| |
| test_device()->SendCommandChannelPacket(event.data()); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(hci_spec::kMaxLEExtendedAdvertisingDataLength, result_data_length); |
| } |
| |
| // If we receive an event marked as incomplete, there is more data coming in |
| // another extended advertising report. We should wait for that data and not |
| // call the peer found callback. |
| TEST_F(ExtendedLowEnergyScannerTest, Incomplete) { |
| size_t data_size = peer(1)->advertising_data().size(); |
| size_t reports_size = report_prefix_size + data_size; |
| size_t packet_size = event_prefix_size + reports_size; |
| |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(1); |
| |
| LEExtendedAdvertisingReportDataWriter report( |
| packet.reports().BackingStorage().begin(), reports_size); |
| peer(1)->FillExtendedAdvertisingReport(report, |
| peer(1)->advertising_data(), |
| /*is_fragmented=*/true, |
| /*is_scan_response=*/false); |
| test_device()->SendCommandChannelPacket(event.data()); |
| |
| bool callback_called = false; |
| set_peer_found_callback( |
| [&](const LowEnergyScanResult& result) { callback_called = true; }); |
| |
| RunUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| } |
| |
| // If we receive an event marked as incomplete truncated, the data was truncated |
| // but we won't be receiving any more advertising reports for this particular |
| // peer. We can go ahead and notify a peer was found with the data we do |
| // currently have. Ensure that we do. |
| // |
| // We specifically use peer(0) here because it is set to be scannable. We want |
| // to make sure that we continue to scan for a scan response, even if the |
| // advertising data got truncated. |
| TEST_F(ExtendedLowEnergyScannerTest, IncompleteTruncated) { |
| size_t data_size = peer(0)->advertising_data().size(); |
| size_t reports_size = report_prefix_size + data_size; |
| size_t packet_size = event_prefix_size + reports_size; |
| |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(1); |
| |
| LEExtendedAdvertisingReportDataWriter report( |
| packet.reports().BackingStorage().begin(), reports_size); |
| peer(0)->FillExtendedAdvertisingReport(report, |
| peer(0)->advertising_data(), |
| /*is_fragmented=*/false, |
| /*is_scan_response=*/false); |
| report.event_type().data_status().Write( |
| LEAdvertisingDataStatus::INCOMPLETE_TRUNCATED); |
| test_device()->SendCommandChannelPacket(event.data()); |
| |
| bool callback_called = false; |
| set_peer_found_callback( |
| [&](const LowEnergyScanResult& result) { callback_called = true; }); |
| |
| RunUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| } |
| |
| // If we receive an event marked as incomplete truncated, the data was truncated |
| // but we won't be receiving any more advertising reports for this particular |
| // peer. We can go ahead and notify a peer was found with the data we do |
| // currently have. |
| // |
| // We specifically use peer(1) here because it is not set to be scannable. We |
| // want to make sure that we report the peer found right away if the peer isn't |
| // scannable, essentially treating this event as if the advertising data was |
| // complete. |
| TEST_F(ExtendedLowEnergyScannerTest, IncompleteTruncatedNonScannable) { |
| size_t data_size = peer(1)->advertising_data().size(); |
| size_t reports_size = report_prefix_size + data_size; |
| size_t packet_size = event_prefix_size + reports_size; |
| |
| auto event = |
| hci::EmbossEventPacket::New<LEExtendedAdvertisingReportSubeventWriter>( |
| hci_spec::kLEMetaEventCode, packet_size); |
| auto packet = event.view_t(reports_size); |
| packet.le_meta_event().subevent_code().Write( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| packet.num_reports().Write(1); |
| |
| LEExtendedAdvertisingReportDataWriter report( |
| packet.reports().BackingStorage().begin(), reports_size); |
| peer(1)->FillExtendedAdvertisingReport(report, |
| peer(1)->advertising_data(), |
| /*is_fragmented=*/false, |
| /*is_scan_response=*/false); |
| report.event_type().data_status().Write( |
| LEAdvertisingDataStatus::INCOMPLETE_TRUNCATED); |
| test_device()->SendCommandChannelPacket(event.data()); |
| |
| bool callback_called = false; |
| set_peer_found_callback( |
| [&](const LowEnergyScanResult& result) { callback_called = true; }); |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| } |
| |
| } // namespace bt::hci |