| // Copyright 2017 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 "low_energy_discovery_manager.h" |
| |
| #include <zircon/assert.h> |
| |
| #include "garnet/drivers/bluetooth/lib/hci/legacy_low_energy_scanner.h" |
| #include "garnet/drivers/bluetooth/lib/hci/transport.h" |
| |
| #include "remote_device.h" |
| #include "remote_device_cache.h" |
| |
| namespace btlib { |
| namespace gap { |
| |
| LowEnergyDiscoverySession::LowEnergyDiscoverySession( |
| fxl::WeakPtr<LowEnergyDiscoveryManager> manager) |
| : active_(true), manager_(manager) { |
| ZX_DEBUG_ASSERT(manager_); |
| } |
| |
| LowEnergyDiscoverySession::~LowEnergyDiscoverySession() { |
| ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent()); |
| if (active_) |
| Stop(); |
| } |
| |
| void LowEnergyDiscoverySession::SetResultCallback( |
| DeviceFoundCallback callback) { |
| device_found_callback_ = std::move(callback); |
| if (!manager_) |
| return; |
| for (const auto& cached_device_id : manager_->cached_scan_results()) { |
| auto device = manager_->device_cache()->FindDeviceById(cached_device_id); |
| ZX_DEBUG_ASSERT(device); |
| NotifyDiscoveryResult(*device); |
| } |
| } |
| |
| void LowEnergyDiscoverySession::Stop() { |
| ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent()); |
| ZX_DEBUG_ASSERT(active_); |
| if (manager_) { |
| manager_->RemoveSession(this); |
| } |
| active_ = false; |
| } |
| |
| void LowEnergyDiscoverySession::NotifyDiscoveryResult( |
| const RemoteDevice& device) const { |
| ZX_DEBUG_ASSERT(device.le()); |
| if (device_found_callback_ && |
| filter_.MatchLowEnergyResult(device.le()->advertising_data(), |
| device.connectable(), device.rssi())) { |
| device_found_callback_(device); |
| } |
| } |
| |
| void LowEnergyDiscoverySession::NotifyError() { |
| active_ = false; |
| if (error_callback_) |
| error_callback_(); |
| } |
| |
| LowEnergyDiscoveryManager::LowEnergyDiscoveryManager( |
| Mode mode, fxl::RefPtr<hci::Transport> hci, RemoteDeviceCache* device_cache) |
| : dispatcher_(async_get_default_dispatcher()), |
| device_cache_(device_cache), |
| background_scan_enabled_(false), |
| weak_ptr_factory_(this) { |
| ZX_DEBUG_ASSERT(hci); |
| ZX_DEBUG_ASSERT(dispatcher_); |
| ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent()); |
| ZX_DEBUG_ASSERT(device_cache_); |
| |
| // We currently do not support the Extended Advertising feature. |
| ZX_DEBUG_ASSERT(mode == Mode::kLegacy); |
| |
| scanner_ = |
| std::make_unique<hci::LegacyLowEnergyScanner>(this, hci, dispatcher_); |
| } |
| |
| LowEnergyDiscoveryManager::~LowEnergyDiscoveryManager() { |
| // TODO(armansito): Invalidate all known session objects here. |
| } |
| |
| void LowEnergyDiscoveryManager::StartDiscovery(SessionCallback callback) { |
| ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent()); |
| ZX_DEBUG_ASSERT(callback); |
| bt_log(INFO, "gap-le", "start discovery"); |
| |
| // If a request to start or stop is currently pending then this one will |
| // become pending until the HCI request completes (this does NOT include the |
| // state in which we are stopping and restarting scan in between scan |
| // periods). |
| if (!pending_.empty() || |
| (scanner_->state() == hci::LowEnergyScanner::State::kStopping && |
| sessions_.empty())) { |
| ZX_DEBUG_ASSERT(!scanner_->IsScanning()); |
| pending_.push(std::move(callback)); |
| return; |
| } |
| |
| // If a device scan is already in progress, then the request succeeds (this |
| // includes the state in which we are stopping and restarting scan in between |
| // scan periods). |
| if (!sessions_.empty()) { |
| // Invoke |callback| asynchronously. |
| auto session = AddSession(); |
| async::PostTask(dispatcher_, [callback = std::move(callback), |
| session = std::move(session)]() mutable { |
| callback(std::move(session)); |
| }); |
| return; |
| } |
| |
| pending_.push(std::move(callback)); |
| |
| // If currently scanning in the background, stop it and wait for |
| // OnScanStatus() to initiate the active scan. Otherwise, request an active |
| // scan if the scanner is idle. |
| if (scanner_->IsPassiveScanning()) { |
| ZX_DEBUG_ASSERT(background_scan_enabled_); |
| scanner_->StopScan(); |
| } else if (!background_scan_enabled_ && scanner_->IsIdle()) { |
| StartActiveScan(); |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::EnableBackgroundScan(bool enable) { |
| if (background_scan_enabled_ == enable) { |
| bt_log(TRACE, "gap-le", "background scan already %s", |
| (enable ? "enabled" : "disabled")); |
| return; |
| } |
| |
| background_scan_enabled_ = enable; |
| |
| // Do nothing if an active scan is in progress. |
| if (!sessions_.empty() || !pending_.empty()) { |
| return; |
| } |
| |
| if (enable && scanner_->IsIdle()) { |
| StartPassiveScan(); |
| } else if (!enable && scanner_->IsPassiveScanning()) { |
| scanner_->StopScan(); |
| } |
| // If neither condition is true, we'll apply a scan policy in OnScanStatus(). |
| } |
| |
| std::unique_ptr<LowEnergyDiscoverySession> |
| LowEnergyDiscoveryManager::AddSession() { |
| // Cannot use make_unique here since LowEnergyDiscoverySession has a private |
| // constructor. |
| std::unique_ptr<LowEnergyDiscoverySession> session( |
| new LowEnergyDiscoverySession(weak_ptr_factory_.GetWeakPtr())); |
| ZX_DEBUG_ASSERT(sessions_.find(session.get()) == sessions_.end()); |
| sessions_.insert(session.get()); |
| return session; |
| } |
| |
| void LowEnergyDiscoveryManager::RemoveSession( |
| LowEnergyDiscoverySession* session) { |
| ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent()); |
| ZX_DEBUG_ASSERT(session); |
| |
| // Only active sessions are allowed to call this method. If there is at least |
| // one active session object out there, then we MUST be scanning. |
| ZX_DEBUG_ASSERT(session->active()); |
| |
| ZX_DEBUG_ASSERT(sessions_.find(session) != sessions_.end()); |
| sessions_.erase(session); |
| |
| // Stop scanning if the session count has dropped to zero. |
| if (sessions_.empty()) |
| scanner_->StopScan(); |
| } |
| |
| void LowEnergyDiscoveryManager::OnDeviceFound( |
| const hci::LowEnergyScanResult& result, const common::ByteBuffer& data) { |
| ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent()); |
| |
| // Ignore regular scan results during a passive scan. |
| if (scanner_->IsPassiveScanning()) { |
| return; |
| } |
| |
| auto device = device_cache_->FindDeviceByAddress(result.address); |
| if (!device) { |
| device = device_cache_->NewDevice(result.address, result.connectable); |
| } |
| device->MutLe().SetAdvertisingData(result.rssi, data); |
| |
| cached_scan_results_.insert(device->identifier()); |
| |
| for (const auto& session : sessions_) { |
| session->NotifyDiscoveryResult(*device); |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::OnDirectedAdvertisement( |
| const hci::LowEnergyScanResult& result) { |
| ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent()); |
| |
| // TODO(NET-1572): Resolve the address in the host if it is random and |
| // |result.resolved| is false. |
| bt_log(SPEW, "gap", "Received directed advertisement (address: %s, %s)", |
| result.address.ToString().c_str(), |
| (result.resolved ? "resolved" : "not resolved")); |
| |
| auto device = device_cache_->FindDeviceByAddress(result.address); |
| if (!device) { |
| bt_log(TRACE, "gap", |
| "ignoring connection request from unknown peripheral: %s", |
| result.address.ToString().c_str()); |
| return; |
| } |
| |
| if (!device->le() || !device->le()->bonded()) { |
| bt_log(TRACE, "gap", |
| "rejecting connection request from unbonded peripheral: %s", |
| result.address.ToString().c_str()); |
| return; |
| } |
| |
| // TODO(armansito): We shouldn't always accept connection requests from all |
| // bonded peripherals (e.g. if one is explicitly disconnected). Maybe add an |
| // "auto_connect()" property to RemoteDevice? |
| if (directed_conn_cb_) { |
| directed_conn_cb_(device->identifier()); |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::OnScanStatus( |
| hci::LowEnergyScanner::ScanStatus status) { |
| switch (status) { |
| case hci::LowEnergyScanner::ScanStatus::kFailed: { |
| bt_log(ERROR, "gap-le", "failed to initiate scan!"); |
| |
| // Clear all sessions. |
| auto sessions = std::move(sessions_); |
| for (auto& s : sessions) { |
| s->NotifyError(); |
| } |
| |
| // Report failure on all currently pending requests. If any of the |
| // callbacks issue a retry the new requests will get re-queued and |
| // notified of failure in the same loop here. |
| while (!pending_.empty()) { |
| auto callback = std::move(pending_.front()); |
| pending_.pop(); |
| |
| callback(nullptr); |
| } |
| return; |
| } |
| case hci::LowEnergyScanner::ScanStatus::kPassive: |
| bt_log(SPEW, "gap-le", "passive scan started"); |
| |
| // Stop the background scan if active scan was requested or background |
| // scan was disabled while waiting for the scan to start. If an active |
| // scan was requested then the active scan we'll start it once the passive |
| // scan stops. |
| if (!pending_.empty() || !background_scan_enabled_) { |
| scanner_->StopScan(); |
| } |
| return; |
| case hci::LowEnergyScanner::ScanStatus::kActive: |
| bt_log(SPEW, "gap-le", "active scan started"); |
| |
| // Create and register all sessions before notifying the clients. We do |
| // this so that the reference count is incremented for all new sessions |
| // before the callbacks execute, to prevent a potential case in which a |
| // callback stops its session immediately which could cause the reference |
| // count to drop the zero before all clients receive their session object. |
| if (!pending_.empty()) { |
| size_t count = pending_.size(); |
| std::unique_ptr<LowEnergyDiscoverySession> new_sessions[count]; |
| std::generate(new_sessions, new_sessions + count, |
| [this] { return AddSession(); }); |
| for (size_t i = 0; i < count; i++) { |
| auto callback = std::move(pending_.front()); |
| pending_.pop(); |
| |
| callback(std::move(new_sessions[i])); |
| } |
| } |
| ZX_DEBUG_ASSERT(pending_.empty()); |
| return; |
| case hci::LowEnergyScanner::ScanStatus::kStopped: |
| bt_log(TRACE, "gap-le", "stopped scanning"); |
| |
| cached_scan_results_.clear(); |
| |
| // Some clients might have requested to start scanning while we were |
| // waiting for it to stop. Restart active scanning if that is the case. |
| // Otherwise start a background scan, if enabled. |
| if (!pending_.empty()) { |
| bt_log(TRACE, "gap-le", "initiate active scan"); |
| StartActiveScan(); |
| } else if (background_scan_enabled_) { |
| bt_log(TRACE, "gap-le", "initiate background scan"); |
| StartPassiveScan(); |
| } |
| return; |
| case hci::LowEnergyScanner::ScanStatus::kComplete: |
| bt_log(SPEW, "gap-le", "end of scan period"); |
| cached_scan_results_.clear(); |
| |
| // If |sessions_| is empty this is because sessions were stopped while the |
| // scanner was shutting down after the end of the scan period. Restart the |
| // scan as long as clients are waiting for it. |
| if (!sessions_.empty() || !pending_.empty()) { |
| bt_log(SPEW, "gap-le", "continuing periodic scan"); |
| StartActiveScan(); |
| } else if (background_scan_enabled_) { |
| bt_log(SPEW, "gap-le", "continuing periodic background scan"); |
| StartPassiveScan(); |
| } |
| return; |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::StartScan(bool active) { |
| auto cb = [self = weak_ptr_factory_.GetWeakPtr()](auto status) { |
| if (self) |
| self->OnScanStatus(status); |
| }; |
| |
| // TODO(armansito): A client that is interested in scanning nearby beacons and |
| // calculating proximity based on RSSI changes may want to disable duplicate |
| // filtering. We generally shouldn't allow this unless a client has the |
| // capability for it. Processing all HCI events containing advertising reports |
| // will both generate a lot of bus traffic and performing duplicate filtering |
| // on the host will take away CPU cycles from other things. It's a valid use |
| // case but needs proper management. For now we always make the controller |
| // filter duplicate reports. |
| |
| // See Vol 3, Part C, 9.3.11 "Connection Establishment Timing Parameters". |
| uint16_t interval, window; |
| if (active) { |
| interval = kLEScanFastInterval; |
| window = kLEScanFastWindow; |
| } else { |
| interval = kLEScanSlowInterval1; |
| window = kLEScanSlowWindow1; |
| // TODO(armansito): Use the controller whitelist to filter advertisements. |
| } |
| |
| // Since we use duplicate filtering, we stop and start the scan periodically |
| // to re-process advertisements. We use the minimum required scan period for |
| // general discovery (by default; |scan_period_| can be modified, e.g. by unit |
| // tests). |
| scanner_->StartScan(active, interval, window, true /* filter_duplicates */, |
| hci::LEScanFilterPolicy::kNoWhiteList, scan_period_, cb); |
| } |
| |
| } // namespace gap |
| } // namespace btlib |