| // 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 <lib/async/cpp/time.h> |
| #include <lib/async/default.h> |
| #include <lib/fit/function.h> |
| #include <zircon/assert.h> |
| |
| #include "peer.h" |
| #include "peer_cache.h" |
| #include "src/connectivity/bluetooth/core/bt-host/transport/transport.h" |
| |
| namespace bt::gap { |
| |
| constexpr uint16_t kLEActiveScanInterval = 80; // 50ms |
| constexpr uint16_t kLEActiveScanWindow = 24; // 15ms |
| constexpr uint16_t kLEPassiveScanInterval = kLEScanSlowInterval1; |
| constexpr uint16_t kLEPassiveScanWindow = kLEScanSlowWindow1; |
| |
| const char* kInspectPausedCountPropertyName = "paused"; |
| const char* kInspectStatePropertyName = "state"; |
| const char* kInspectFailedCountPropertyName = "failed_count"; |
| const char* kInspectScanIntervalPropertyName = "scan_interval_ms"; |
| const char* kInspectScanWindowPropertyName = "scan_window_ms"; |
| |
| LowEnergyDiscoverySession::LowEnergyDiscoverySession( |
| bool active, fxl::WeakPtr<LowEnergyDiscoveryManager> manager) |
| : alive_(true), active_(active), manager_(manager) { |
| ZX_ASSERT(manager_); |
| } |
| |
| LowEnergyDiscoverySession::~LowEnergyDiscoverySession() { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| if (alive_) { |
| Stop(); |
| } |
| } |
| |
| void LowEnergyDiscoverySession::SetResultCallback(PeerFoundCallback callback) { |
| peer_found_callback_ = std::move(callback); |
| if (!manager_) |
| return; |
| for (PeerId cached_peer_id : manager_->cached_scan_results()) { |
| auto peer = manager_->peer_cache()->FindById(cached_peer_id); |
| // Ignore peers that have since been removed from the peer cache. |
| if (!peer) { |
| bt_log(TRACE, "gap", "Ignoring cached scan result for peer %s missing from peer cache", |
| bt_str(cached_peer_id)); |
| continue; |
| } |
| NotifyDiscoveryResult(*peer); |
| } |
| } |
| |
| void LowEnergyDiscoverySession::Stop() { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| ZX_DEBUG_ASSERT(alive_); |
| if (manager_) { |
| manager_->RemoveSession(this); |
| } |
| alive_ = false; |
| } |
| |
| void LowEnergyDiscoverySession::NotifyDiscoveryResult(const Peer& peer) const { |
| ZX_ASSERT(peer.le()); |
| |
| if (!alive_ || !peer_found_callback_) { |
| return; |
| } |
| |
| if (filter_.MatchLowEnergyResult(peer.le()->advertising_data(), peer.connectable(), |
| peer.rssi())) { |
| peer_found_callback_(peer); |
| } |
| } |
| |
| void LowEnergyDiscoverySession::NotifyError() { |
| alive_ = false; |
| if (error_callback_) { |
| error_callback_(); |
| } |
| } |
| |
| LowEnergyDiscoveryManager::LowEnergyDiscoveryManager(fxl::WeakPtr<hci::Transport> hci, |
| hci::LowEnergyScanner* scanner, |
| PeerCache* peer_cache) |
| : dispatcher_(async_get_default_dispatcher()), |
| state_(State::kIdle, StateToString), |
| peer_cache_(peer_cache), |
| paused_count_(0), |
| scanner_(scanner), |
| weak_ptr_factory_(this) { |
| ZX_DEBUG_ASSERT(hci); |
| ZX_DEBUG_ASSERT(dispatcher_); |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| ZX_DEBUG_ASSERT(peer_cache_); |
| ZX_DEBUG_ASSERT(scanner_); |
| |
| scanner_->set_delegate(this); |
| } |
| |
| LowEnergyDiscoveryManager::~LowEnergyDiscoveryManager() { |
| scanner_->set_delegate(nullptr); |
| |
| DeactivateAndNotifySessions(); |
| } |
| |
| void LowEnergyDiscoveryManager::StartDiscovery(bool active, SessionCallback callback) { |
| ZX_ASSERT(thread_checker_.is_thread_valid()); |
| ZX_ASSERT(callback); |
| bt_log(INFO, "gap-le", "start %s discovery", active ? "active" : "passive"); |
| |
| // 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, in which case session_ will not be empty. |
| // |
| // If the scan needs to be upgraded to an active scan, it will be handled in OnScanStatus() when |
| // the HCI request completes. |
| if (!pending_.empty() || |
| (scanner_->state() == hci::LowEnergyScanner::State::kStopping && sessions_.empty())) { |
| ZX_ASSERT(!scanner_->IsScanning()); |
| pending_.push_back(DiscoveryRequest{.active = active, .callback = std::move(callback)}); |
| return; |
| } |
| |
| // If a peer 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()) { |
| if (active) { |
| // If this is the first active session, stop scanning and wait for OnScanStatus() to initiate |
| // active scan. |
| if (!std::any_of(sessions_.begin(), sessions_.end(), [](auto s) { return s->active_; })) { |
| StopScan(); |
| } |
| } |
| |
| auto session = AddSession(active); |
| async::PostTask(dispatcher_, |
| [callback = std::move(callback), session = std::move(session)]() mutable { |
| callback(std::move(session)); |
| }); |
| return; |
| } |
| |
| pending_.push_back({.active = active, .callback = std::move(callback)}); |
| |
| if (paused()) { |
| return; |
| } |
| |
| // If the scanner is not idle, it is starting/stopping, and the appropriate scanning will be |
| // initiated in OnScanStatus(). |
| if (scanner_->IsIdle()) { |
| StartScan(active); |
| } |
| } |
| |
| LowEnergyDiscoveryManager::PauseToken LowEnergyDiscoveryManager::PauseDiscovery() { |
| if (!paused()) { |
| bt_log(TRACE, "gap-le", "Pausing discovery"); |
| StopScan(); |
| } |
| |
| paused_count_.Set(*paused_count_ + 1); |
| |
| return PauseToken([this, self = weak_ptr_factory_.GetWeakPtr()]() { |
| if (!self) { |
| return; |
| } |
| |
| ZX_ASSERT(paused()); |
| paused_count_.Set(*paused_count_ - 1); |
| if (*paused_count_ == 0) { |
| ResumeDiscovery(); |
| } |
| }); |
| } |
| |
| bool LowEnergyDiscoveryManager::discovering() const { |
| return std::any_of(sessions_.begin(), sessions_.end(), [](auto& s) { return s->active(); }); |
| } |
| |
| void LowEnergyDiscoveryManager::AttachInspect(inspect::Node& parent, std::string name) { |
| inspect_.node = parent.CreateChild(name); |
| paused_count_.AttachInspect(inspect_.node, kInspectPausedCountPropertyName); |
| state_.AttachInspect(inspect_.node, kInspectStatePropertyName); |
| inspect_.failed_count = inspect_.node.CreateUint(kInspectFailedCountPropertyName, 0); |
| inspect_.scan_interval_ms = inspect_.node.CreateDouble(kInspectScanIntervalPropertyName, 0); |
| inspect_.scan_window_ms = inspect_.node.CreateDouble(kInspectScanWindowPropertyName, 0); |
| } |
| |
| std::string LowEnergyDiscoveryManager::StateToString(State state) { |
| switch (state) { |
| case State::kIdle: |
| return "Idle"; |
| case State::kStarting: |
| return "Starting"; |
| case State::kActive: |
| return "Active"; |
| case State::kPassive: |
| return "Passive"; |
| case State::kStopping: |
| return "Stopping"; |
| } |
| } |
| |
| std::unique_ptr<LowEnergyDiscoverySession> LowEnergyDiscoveryManager::AddSession(bool active) { |
| // Cannot use make_unique here since LowEnergyDiscoverySession has a private |
| // constructor. |
| std::unique_ptr<LowEnergyDiscoverySession> session( |
| new LowEnergyDiscoverySession(active, weak_ptr_factory_.GetWeakPtr())); |
| sessions_.push_back(session.get()); |
| return session; |
| } |
| |
| void LowEnergyDiscoveryManager::RemoveSession(LowEnergyDiscoverySession* session) { |
| ZX_ASSERT(thread_checker_.is_thread_valid()); |
| ZX_ASSERT(session); |
| |
| // Only alive sessions are allowed to call this method. If there is at least |
| // one alive session object out there, then we MUST be scanning. |
| ZX_ASSERT(session->alive()); |
| |
| auto iter = std::find(sessions_.begin(), sessions_.end(), session); |
| ZX_ASSERT(iter != sessions_.end()); |
| |
| bool active = session->active(); |
| |
| sessions_.erase(iter); |
| |
| bool last_active = active && std::none_of(sessions_.begin(), sessions_.end(), |
| [](auto& s) { return s->active_; }); |
| |
| // Stop scanning if the session count has dropped to zero or the scan type needs to be downgraded |
| // to passive. |
| if (sessions_.empty() || last_active) { |
| bt_log(TRACE, "gap-le", "Last %sdiscovery session removed, stopping scan (sessions: %zu)", |
| last_active ? "active " : "", sessions_.size()); |
| StopScan(); |
| return; |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::OnPeerFound(const hci::LowEnergyScanResult& result, |
| const ByteBuffer& data) { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| bt_log(DEBUG, "gap-le", "peer found (address: %s, connectable: %d)", bt_str(result.address), |
| result.connectable); |
| |
| auto peer = peer_cache_->FindByAddress(result.address); |
| if (peer && peer->connectable() && peer->le() && connectable_cb_) { |
| bt_log(TRACE, "gap-le", "found connectable peer (id: %s)", bt_str(peer->identifier())); |
| connectable_cb_(peer); |
| } |
| |
| // Don't notify sessions of unknown LE peers during passive scan. |
| if (scanner_->IsPassiveScanning() && (!peer || !peer->le())) { |
| return; |
| } |
| |
| // Create a new entry if we found the device during general discovery. |
| if (!peer) { |
| peer = peer_cache_->NewPeer(result.address, result.connectable); |
| ZX_ASSERT(peer); |
| } else if (!peer->connectable() && result.connectable) { |
| bt_log(DEBUG, "gap-le", |
| "received connectable advertisement from previously non-connectable peer (address: %s, " |
| "peer: %s)", |
| bt_str(result.address), bt_str(peer->identifier())); |
| peer->set_connectable(true); |
| } |
| |
| peer->MutLe().SetAdvertisingData(result.rssi, data, async::Now(async_get_default_dispatcher())); |
| |
| cached_scan_results_.insert(peer->identifier()); |
| |
| for (auto iter = sessions_.begin(); iter != sessions_.end();) { |
| // The session may be erased by the result handler, so we need to get the next iterator before |
| // iter is invalidated. |
| auto next = std::next(iter); |
| auto session = *iter; |
| session->NotifyDiscoveryResult(*peer); |
| iter = next; |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::OnDirectedAdvertisement(const hci::LowEnergyScanResult& result) { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| |
| bt_log(TRACE, "gap-le", "Received directed advertisement (address: %s, %s)", |
| result.address.ToString().c_str(), (result.resolved ? "resolved" : "not resolved")); |
| |
| auto peer = peer_cache_->FindByAddress(result.address); |
| if (!peer) { |
| bt_log(DEBUG, "gap-le", "ignoring connection request from unknown peripheral: %s", |
| result.address.ToString().c_str()); |
| return; |
| } |
| |
| if (!peer->le()) { |
| bt_log(DEBUG, "gap-le", "rejecting connection request from non-LE peripheral: %s", |
| result.address.ToString().c_str()); |
| return; |
| } |
| |
| if (peer->connectable() && connectable_cb_) { |
| connectable_cb_(peer); |
| } |
| |
| // Only notify passive sessions. |
| for (auto iter = sessions_.begin(); iter != sessions_.end();) { |
| // The session may be erased by the result handler, so we need to get the next iterator before |
| // iter is invalidated. |
| auto next = std::next(iter); |
| auto session = *iter; |
| if (!session->active()) { |
| session->NotifyDiscoveryResult(*peer); |
| } |
| iter = next; |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::OnScanStatus(hci::LowEnergyScanner::ScanStatus status) { |
| switch (status) { |
| case hci::LowEnergyScanner::ScanStatus::kFailed: |
| OnScanFailed(); |
| return; |
| case hci::LowEnergyScanner::ScanStatus::kPassive: |
| OnPassiveScanStarted(); |
| return; |
| case hci::LowEnergyScanner::ScanStatus::kActive: |
| OnActiveScanStarted(); |
| return; |
| case hci::LowEnergyScanner::ScanStatus::kStopped: |
| OnScanStopped(); |
| return; |
| case hci::LowEnergyScanner::ScanStatus::kComplete: |
| OnScanComplete(); |
| return; |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::OnScanFailed() { |
| bt_log(ERROR, "gap-le", "failed to initiate scan!"); |
| |
| inspect_.failed_count.Add(1); |
| DeactivateAndNotifySessions(); |
| |
| // 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 request = std::move(pending_.back()); |
| pending_.pop_back(); |
| request.callback(nullptr); |
| } |
| |
| state_.Set(State::kIdle); |
| } |
| |
| void LowEnergyDiscoveryManager::OnPassiveScanStarted() { |
| bt_log(TRACE, "gap-le", "passive scan started"); |
| |
| state_.Set(State::kPassive); |
| |
| // Stop the passive scan if an active scan was requested while the scan was starting. |
| // The active scan will start in OnScanStopped() once the passive scan stops. |
| if (std::any_of(sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; }) || |
| std::any_of(pending_.begin(), pending_.end(), [](auto& p) { return p.active; })) { |
| bt_log(TRACE, "gap-le", "active scan requested while passive scan was starting"); |
| StopScan(); |
| return; |
| } |
| |
| NotifyPending(); |
| } |
| |
| void LowEnergyDiscoveryManager::OnActiveScanStarted() { |
| bt_log(TRACE, "gap-le", "active scan started"); |
| state_.Set(State::kActive); |
| NotifyPending(); |
| } |
| |
| void LowEnergyDiscoveryManager::OnScanStopped() { |
| bt_log(DEBUG, "gap-le", "stopped scanning (paused: %d, pending: %zu, sessions: %zu)", paused(), |
| pending_.size(), sessions_.size()); |
| |
| state_.Set(State::kIdle); |
| cached_scan_results_.clear(); |
| |
| if (paused()) { |
| return; |
| } |
| |
| if (!sessions_.empty()) { |
| bt_log(DEBUG, "gap-le", "initiating scanning"); |
| bool active = |
| std::any_of(sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; }); |
| StartScan(active); |
| return; |
| } |
| |
| // Some clients might have requested to start scanning while we were |
| // waiting for it to stop. Restart scanning if that is the case. |
| if (!pending_.empty()) { |
| bt_log(DEBUG, "gap-le", "initiating scanning"); |
| bool active = std::any_of(pending_.begin(), pending_.end(), [](auto& p) { return p.active; }); |
| StartScan(active); |
| return; |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::OnScanComplete() { |
| bt_log(TRACE, "gap-le", "end of scan period"); |
| |
| state_.Set(State::kIdle); |
| cached_scan_results_.clear(); |
| |
| if (paused()) { |
| return; |
| } |
| |
| // 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. |
| ResumeDiscovery(); |
| } |
| |
| void LowEnergyDiscoveryManager::NotifyPending() { |
| // 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::vector<std::unique_ptr<LowEnergyDiscoverySession>> new_sessions(count); |
| std::generate(new_sessions.begin(), new_sessions.end(), |
| [this, i = size_t{0}]() mutable { return AddSession(pending_[i++].active); }); |
| |
| for (size_t i = count - 1; i < count; i--) { |
| auto cb = std::move(pending_.back().callback); |
| pending_.pop_back(); |
| cb(std::move(new_sessions[i])); |
| } |
| } |
| ZX_ASSERT(pending_.empty()); |
| } |
| |
| 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. |
| hci::LowEnergyScanner::ScanOptions options{ |
| .active = active, |
| .filter_duplicates = true, |
| .filter_policy = hci_spec::LEScanFilterPolicy::kNoFilterAcceptList, |
| .period = scan_period_, |
| .scan_response_timeout = kLEScanResponseTimeout, |
| }; |
| |
| // See Vol 3, Part C, 9.3.11 "Connection Establishment Timing Parameters". |
| if (active) { |
| options.interval = kLEActiveScanInterval; |
| options.window = kLEActiveScanWindow; |
| } else { |
| options.interval = kLEPassiveScanInterval; |
| options.window = kLEPassiveScanWindow; |
| // TODO(armansito): Use the controller filter accept policy 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). |
| state_.Set(State::kStarting); |
| scanner_->StartScan(options, std::move(cb)); |
| |
| inspect_.scan_interval_ms.Set(HciScanIntervalToMs(options.interval)); |
| inspect_.scan_window_ms.Set(HciScanWindowToMs(options.window)); |
| } |
| |
| void LowEnergyDiscoveryManager::StopScan() { |
| state_.Set(State::kStopping); |
| scanner_->StopScan(); |
| } |
| |
| void LowEnergyDiscoveryManager::ResumeDiscovery() { |
| ZX_ASSERT(!paused()); |
| |
| if (!scanner_->IsIdle()) { |
| bt_log(TRACE, "gap-le", "attempt to resume discovery when it is not idle"); |
| return; |
| } |
| |
| if (!sessions_.empty()) { |
| bt_log(TRACE, "gap-le", "resuming scan"); |
| bool active = |
| std::any_of(sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; }); |
| StartScan(active); |
| return; |
| } |
| |
| if (!pending_.empty()) { |
| bt_log(TRACE, "gap-le", "starting scan"); |
| bool active = std::any_of(pending_.begin(), pending_.end(), [](auto& s) { return s.active; }); |
| StartScan(active); |
| return; |
| } |
| } |
| |
| void LowEnergyDiscoveryManager::DeactivateAndNotifySessions() { |
| // If there are any active sessions we invalidate by notifying of an error. |
| |
| // We move the initial set and notify those, if any error callbacks create |
| // additional sessions they will be added to pending_ |
| auto sessions = std::move(sessions_); |
| for (const auto& session : sessions) { |
| if (session->alive()) { |
| session->NotifyError(); |
| } |
| } |
| |
| // Due to the move, sessions_ should be empty before the loop and any |
| // callbacks will add sessions to pending_ so it should be empty |
| // afterwards as well. |
| ZX_ASSERT(sessions_.empty()); |
| } |
| |
| } // namespace bt::gap |