| // 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 <pw_bluetooth/hci_common.emb.h> |
| |
| namespace bt::hci { |
| |
| using pw::bluetooth::emboss::BdAddrView; |
| using pw::bluetooth::emboss::GenericEnableParam; |
| using pw::bluetooth::emboss::LEAdvertisingDataStatus; |
| using pw::bluetooth::emboss::LEExtendedAddressType; |
| using pw::bluetooth::emboss::LEExtendedAdvertisingReportDataView; |
| using pw::bluetooth::emboss::LEExtendedAdvertisingReportSubeventView; |
| using pw::bluetooth::emboss::LEExtendedDuplicateFilteringOption; |
| using pw::bluetooth::emboss::LEMetaEventView; |
| using pw::bluetooth::emboss::LEScanType; |
| using pw::bluetooth::emboss::LESetExtendedScanEnableCommandWriter; |
| using pw::bluetooth::emboss::LESetExtendedScanParametersCommandWriter; |
| using pw::bluetooth::emboss::MakeLEExtendedAdvertisingReportDataView; |
| |
| ExtendedLowEnergyScanner::ExtendedLowEnergyScanner( |
| LocalAddressDelegate* local_addr_delegate, |
| Transport::WeakPtr transport, |
| pw::async::Dispatcher& pw_dispatcher) |
| : LowEnergyScanner( |
| local_addr_delegate, std::move(transport), pw_dispatcher) { |
| event_handler_id_ = hci()->command_channel()->AddLEMetaEventHandler( |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode, |
| [this](const EmbossEventPacket& event) { |
| OnExtendedAdvertisingReportEvent(event); |
| return hci::CommandChannel::EventCallbackResult::kContinue; |
| }); |
| } |
| ExtendedLowEnergyScanner::~ExtendedLowEnergyScanner() { |
| // This object is probably being destroyed because the stack is shutting down, |
| // in which case the HCI layer may have already been destroyed. |
| if (!hci().is_alive() || !hci()->command_channel()) { |
| return; |
| } |
| |
| hci()->command_channel()->RemoveEventHandler(event_handler_id_); |
| StopScan(); |
| } |
| |
| bool ExtendedLowEnergyScanner::StartScan(const ScanOptions& options, |
| ScanStatusCallback callback) { |
| BT_ASSERT(options.interval >= hci_spec::kLEExtendedScanIntervalMin); |
| BT_ASSERT(options.interval <= hci_spec::kLEExtendedScanIntervalMax); |
| BT_ASSERT(options.window >= hci_spec::kLEExtendedScanIntervalMin); |
| BT_ASSERT(options.window <= hci_spec::kLEExtendedScanIntervalMax); |
| |
| return LowEnergyScanner::StartScan(options, std::move(callback)); |
| } |
| |
| EmbossCommandPacket ExtendedLowEnergyScanner::BuildSetScanParametersPacket( |
| const DeviceAddress& local_address, const ScanOptions& options) { |
| // LESetExtendedScanParametersCommand contains a variable amount of data, |
| // depending on how many bits are set within the scanning_phys parameter. As |
| // such, we must first calculate the size of the variable data before |
| // allocating the packet. |
| |
| // we scan on the LE 1M PHY and the LE Coded PHY |
| constexpr size_t num_phys = 2; |
| constexpr size_t fixed_size = pw::bluetooth::emboss:: |
| LESetExtendedScanParametersCommand::MinSizeInBytes(); |
| constexpr size_t variable_size = pw::bluetooth::emboss:: |
| LESetExtendedScanParametersData::IntrinsicSizeInBytes(); |
| constexpr size_t packet_size = fixed_size + (num_phys * variable_size); |
| |
| auto packet = |
| hci::EmbossCommandPacket::New<LESetExtendedScanParametersCommandWriter>( |
| hci_spec::kLESetExtendedScanParameters, packet_size); |
| auto params = packet.view_t(); |
| |
| params.scanning_filter_policy().Write(options.filter_policy); |
| params.own_address_type().Write( |
| DeviceAddress::DeviceAddrToLEOwnAddr(local_address.type())); |
| |
| // For maximum compatibility, Sapphire scans on all available PHYs. |
| params.scanning_phys().le_1m().Write(true); |
| params.scanning_phys().le_coded().Write(true); |
| |
| for (size_t i = 0; i < num_phys; i++) { |
| params.data()[i].scan_type().Write(LEScanType::PASSIVE); |
| if (options.active) { |
| params.data()[i].scan_type().Write(LEScanType::ACTIVE); |
| } |
| |
| params.data()[i].scan_interval().Write(options.interval); |
| params.data()[i].scan_window().Write(options.window); |
| } |
| |
| return packet; |
| } |
| |
| EmbossCommandPacket ExtendedLowEnergyScanner::BuildEnablePacket( |
| const ScanOptions& options, GenericEnableParam enable) { |
| auto packet = EmbossCommandPacket::New<LESetExtendedScanEnableCommandWriter>( |
| hci_spec::kLESetExtendedScanEnable); |
| auto params = packet.view_t(); |
| |
| params.scanning_enabled().Write(enable); |
| params.filter_duplicates().Write( |
| LEExtendedDuplicateFilteringOption::DISABLED); |
| if (options.filter_duplicates) { |
| params.filter_duplicates().Write( |
| LEExtendedDuplicateFilteringOption::ENABLED); |
| } |
| |
| // The scan duration and period parameters control how long the scan |
| // continues. Setting these values to hci_spec::kNoScanningDuration and |
| // hci_spec::kNoScanningPeriod, respectively, means that scanning continues |
| // indefinitely until the client requests it to stop. |
| params.duration().Write(hci_spec::kNoScanningDuration); |
| params.period().Write(hci_spec::kNoScanningPeriod); |
| |
| return packet; |
| } |
| |
| // Extract all advertising reports from a given HCI LE Extended Advertising |
| // Report event |
| std::vector<LEExtendedAdvertisingReportDataView> |
| ExtendedLowEnergyScanner::ParseAdvertisingReports( |
| const EmbossEventPacket& event) { |
| BT_DEBUG_ASSERT(event.event_code() == hci_spec::kLEMetaEventCode); |
| BT_DEBUG_ASSERT(event.view<LEMetaEventView>().subevent_code().Read() == |
| hci_spec::kLEExtendedAdvertisingReportSubeventCode); |
| size_t reports_size = |
| event.size() - |
| pw::bluetooth::emboss::LEExtendedAdvertisingReportSubeventView:: |
| MinSizeInBytes() |
| .Read(); |
| auto params = event.view<LEExtendedAdvertisingReportSubeventView>( |
| static_cast<int32_t>(reports_size)); |
| |
| uint8_t num_reports = params.num_reports().Read(); |
| std::vector<LEExtendedAdvertisingReportDataView> reports; |
| reports.reserve(num_reports); |
| |
| size_t bytes_read = 0; |
| while (bytes_read < params.reports().BackingStorage().SizeInBytes()) { |
| size_t min_size = pw::bluetooth::emboss::LEExtendedAdvertisingReportData:: |
| MinSizeInBytes(); |
| auto report_prefix = MakeLEExtendedAdvertisingReportDataView( |
| params.reports().BackingStorage().begin() + bytes_read, min_size); |
| |
| uint8_t data_length = report_prefix.data_length().Read(); |
| size_t actual_size = min_size + data_length; |
| |
| size_t bytes_left = |
| params.reports().BackingStorage().SizeInBytes() - bytes_read; |
| if (actual_size > bytes_left) { |
| bt_log(WARN, |
| "hci-le", |
| "parsing advertising reports, next report size %zu bytes, but " |
| "only %zu bytes left", |
| actual_size, |
| bytes_left); |
| break; |
| } |
| |
| auto report = MakeLEExtendedAdvertisingReportDataView( |
| params.reports().BackingStorage().begin() + bytes_read, actual_size); |
| reports.push_back(report); |
| |
| bytes_read += actual_size; |
| } |
| |
| return reports; |
| } |
| |
| static std::tuple<DeviceAddress, bool> BuildDeviceAddress( |
| LEExtendedAddressType report_type, BdAddrView address_view) { |
| DeviceAddress::Type address_type = |
| DeviceAddress::LeAddrToDeviceAddr(report_type); |
| |
| bool resolved = false; |
| switch (report_type) { |
| case LEExtendedAddressType::PUBLIC_IDENTITY: |
| case LEExtendedAddressType::RANDOM_IDENTITY: |
| resolved = true; |
| break; |
| case LEExtendedAddressType::PUBLIC: |
| case LEExtendedAddressType::RANDOM: |
| case LEExtendedAddressType::ANONYMOUS: |
| default: |
| resolved = false; |
| break; |
| } |
| |
| DeviceAddress address = |
| DeviceAddress(address_type, DeviceAddressBytes(address_view)); |
| return std::make_tuple(address, resolved); |
| } |
| |
| void ExtendedLowEnergyScanner::OnExtendedAdvertisingReportEvent( |
| const EmbossEventPacket& event) { |
| if (!IsScanning()) { |
| return; |
| } |
| |
| std::vector<LEExtendedAdvertisingReportDataView> reports = |
| ParseAdvertisingReports(event); |
| for (LEExtendedAdvertisingReportDataView report : reports) { |
| const auto& [address, resolved] = |
| BuildDeviceAddress(report.address_type().Read(), report.address()); |
| |
| bool is_directed = report.event_type().directed().Read(); |
| bool is_connectable = report.event_type().connectable().Read(); |
| bool is_scannable = report.event_type().scannable().Read(); |
| bool is_scan_response = report.event_type().scan_response().Read(); |
| |
| // scan responses without a pending result from an advertising data result |
| // mean they are too late and the timer waiting for them has expired. The |
| // delegate has already been notified and we unfortunately need to drop this |
| // result. |
| if (is_scan_response && !HasPendingResult(address)) { |
| bt_log(DEBUG, "hci-le", "dropping unmatched scan response"); |
| return; |
| } |
| |
| int8_t rssi = report.rssi().Read(); |
| BufferView data(report.data().BackingStorage().begin(), |
| report.data_length().Read()); |
| |
| LowEnergyScanResult result; |
| std::unique_ptr<PendingScanResult> pending = RemovePendingResult(address); |
| if (pending) { |
| result = std::move(pending->result()); |
| } else { |
| result = LowEnergyScanResult(address, resolved, is_connectable); |
| } |
| |
| result.set_resolved(resolved); |
| result.set_rssi(rssi); |
| result.set_tx_power(report.tx_power().Read()); |
| result.set_advertising_sid(report.advertising_sid().Read()); |
| |
| // If the next set of data exceeds the maximum allowed in an extended |
| // advertising data payload, take as much as we can and report it back. |
| size_t size_after_add = result.data().size() + data.size(); |
| if (size_after_add > hci_spec::kMaxLEExtendedAdvertisingDataLength) { |
| bt_log( |
| WARN, |
| "hci-le", |
| "advertising data for (%s) too long (actual: %zu, max: %zu)! Ignoring rest.", |
| bt_str(address), |
| size_after_add, |
| hci_spec::kMaxLEExtendedAdvertisingDataLength); |
| |
| size_t bytes_allowed = |
| hci_spec::kMaxLEExtendedAdvertisingDataLength - result.data().size(); |
| BufferView truncated_data = |
| BufferView(report.data().BackingStorage().begin(), bytes_allowed); |
| result.AppendData(truncated_data); |
| |
| delegate()->OnPeerFound(result); |
| continue; |
| } |
| |
| result.AppendData(data); |
| |
| LEAdvertisingDataStatus data_status = |
| report.event_type().data_status().Read(); |
| if (data_status == LEAdvertisingDataStatus::INCOMPLETE) { |
| // There is more data coming in another extended advertising PDU so we |
| // just wait for it |
| AddPendingResult(std::move(result)); |
| continue; |
| } |
| |
| // Incoming data was truncated and we won't receive the rest. Nothing we can |
| // do about that so just notify the delegate with the data we currently |
| // have. |
| if (data_status == LEAdvertisingDataStatus::INCOMPLETE_TRUNCATED) { |
| bt_log(WARN, |
| "hci-le", |
| "data for %s truncated to %zu bytes", |
| bt_str(address), |
| result.data().size()); |
| } |
| |
| if (is_directed) { |
| delegate()->OnDirectedAdvertisement(result); |
| continue; |
| } |
| |
| if (IsActiveScanning() && is_scan_response) { |
| delegate()->OnPeerFound(result); |
| continue; |
| } |
| |
| if (IsActiveScanning() && is_scannable) { |
| // We need to wait for a scan response. Scan responses have the |
| // scannable bit set so it's important that this if statement comes |
| // after the one checking for a scan response. |
| AddPendingResult(std::move(result)); |
| continue; |
| } |
| |
| delegate()->OnPeerFound(result); |
| } |
| } |
| |
| } // namespace bt::hci |