xref: /aosp_15_r20/external/pigweed/pw_bluetooth_sapphire/host/gap/low_energy_discovery_manager.cc (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1 // Copyright 2023 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14 
15 #include "pw_bluetooth_sapphire/internal/host/gap/low_energy_discovery_manager.h"
16 
17 #include <lib/fit/function.h>
18 
19 #include "pw_bluetooth_sapphire/internal/host/common/assert.h"
20 #include "pw_bluetooth_sapphire/internal/host/gap/peer.h"
21 #include "pw_bluetooth_sapphire/internal/host/gap/peer_cache.h"
22 #include "pw_bluetooth_sapphire/internal/host/transport/transport.h"
23 
24 namespace bt::gap {
25 
26 constexpr uint16_t kLEActiveScanInterval = 80;  // 50ms
27 constexpr uint16_t kLEActiveScanWindow = 24;    // 15ms
28 constexpr uint16_t kLEPassiveScanInterval = kLEScanSlowInterval1;
29 constexpr uint16_t kLEPassiveScanWindow = kLEScanSlowWindow1;
30 
31 const char* kInspectPausedCountPropertyName = "paused";
32 const char* kInspectStatePropertyName = "state";
33 const char* kInspectFailedCountPropertyName = "failed_count";
34 const char* kInspectScanIntervalPropertyName = "scan_interval_ms";
35 const char* kInspectScanWindowPropertyName = "scan_window_ms";
36 
LowEnergyDiscoverySession(bool active,LowEnergyDiscoveryManager::WeakPtr manager)37 LowEnergyDiscoverySession::LowEnergyDiscoverySession(
38     bool active, LowEnergyDiscoveryManager::WeakPtr manager)
39     : alive_(true), active_(active), manager_(std::move(manager)) {
40   PW_CHECK(manager_.is_alive());
41 }
42 
~LowEnergyDiscoverySession()43 LowEnergyDiscoverySession::~LowEnergyDiscoverySession() {
44   if (alive_) {
45     Stop();
46   }
47 }
48 
SetResultCallback(PeerFoundCallback callback)49 void LowEnergyDiscoverySession::SetResultCallback(PeerFoundCallback callback) {
50   peer_found_callback_ = std::move(callback);
51   if (!manager_.is_alive())
52     return;
53   for (PeerId cached_peer_id : manager_->cached_scan_results()) {
54     auto peer = manager_->peer_cache()->FindById(cached_peer_id);
55     // Ignore peers that have since been removed from the peer cache.
56     if (!peer) {
57       bt_log(TRACE,
58              "gap",
59              "Ignoring cached scan result for peer %s missing from peer cache",
60              bt_str(cached_peer_id));
61       continue;
62     }
63     NotifyDiscoveryResult(*peer);
64   }
65 }
66 
Stop()67 void LowEnergyDiscoverySession::Stop() {
68   PW_DCHECK(alive_);
69   if (manager_.is_alive()) {
70     manager_->RemoveSession(this);
71   }
72   alive_ = false;
73 }
74 
NotifyDiscoveryResult(const Peer & peer) const75 void LowEnergyDiscoverySession::NotifyDiscoveryResult(const Peer& peer) const {
76   PW_CHECK(peer.le());
77 
78   if (!alive_ || !peer_found_callback_) {
79     return;
80   }
81 
82   if (filter_.MatchLowEnergyResult(peer.le()->parsed_advertising_data(),
83                                    peer.connectable(),
84                                    peer.rssi())) {
85     peer_found_callback_(peer);
86   }
87 }
88 
NotifyError()89 void LowEnergyDiscoverySession::NotifyError() {
90   alive_ = false;
91   if (error_callback_) {
92     error_callback_();
93   }
94 }
95 
LowEnergyDiscoveryManager(hci::LowEnergyScanner * scanner,PeerCache * peer_cache,pw::async::Dispatcher & dispatcher)96 LowEnergyDiscoveryManager::LowEnergyDiscoveryManager(
97     hci::LowEnergyScanner* scanner,
98     PeerCache* peer_cache,
99     pw::async::Dispatcher& dispatcher)
100     : WeakSelf(this),
101       dispatcher_(dispatcher),
102       state_(State::kIdle, StateToString),
103       peer_cache_(peer_cache),
104       paused_count_(0),
105       scanner_(scanner) {
106   PW_DCHECK(peer_cache_);
107   PW_DCHECK(scanner_);
108 
109   scanner_->set_delegate(this);
110 }
111 
~LowEnergyDiscoveryManager()112 LowEnergyDiscoveryManager::~LowEnergyDiscoveryManager() {
113   scanner_->set_delegate(nullptr);
114 
115   DeactivateAndNotifySessions();
116 }
117 
StartDiscovery(bool active,SessionCallback callback)118 void LowEnergyDiscoveryManager::StartDiscovery(bool active,
119                                                SessionCallback callback) {
120   PW_CHECK(callback);
121   bt_log(INFO, "gap-le", "start %s discovery", active ? "active" : "passive");
122 
123   // If a request to start or stop is currently pending then this one will
124   // become pending until the HCI request completes. This does NOT include the
125   // state in which we are stopping and restarting scan in between scan
126   // periods, in which case session_ will not be empty.
127   //
128   // If the scan needs to be upgraded to an active scan, it will be handled in
129   // OnScanStatus() when the HCI request completes.
130   if (!pending_.empty() ||
131       (scanner_->state() == hci::LowEnergyScanner::State::kStopping &&
132        sessions_.empty())) {
133     PW_CHECK(!scanner_->IsScanning());
134     pending_.push_back(
135         DiscoveryRequest{.active = active, .callback = std::move(callback)});
136     return;
137   }
138 
139   // If a peer scan is already in progress, then the request succeeds (this
140   // includes the state in which we are stopping and restarting scan in between
141   // scan periods).
142   if (!sessions_.empty()) {
143     if (active) {
144       // If this is the first active session, stop scanning and wait for
145       // OnScanStatus() to initiate active scan.
146       if (!std::any_of(sessions_.begin(), sessions_.end(), [](auto s) {
147             return s->active_;
148           })) {
149         StopScan();
150       }
151     }
152 
153     auto session = AddSession(active);
154     // Post the callback instead of calling it synchronously to avoid bugs
155     // caused by client code not expecting this.
156     (void)heap_dispatcher_.Post(
157         [cb = std::move(callback), discovery_session = std::move(session)](
158             pw::async::Context /*ctx*/, pw::Status status) mutable {
159           if (status.ok()) {
160             cb(std::move(discovery_session));
161           }
162         });
163     return;
164   }
165 
166   pending_.push_back({.active = active, .callback = std::move(callback)});
167 
168   if (paused()) {
169     return;
170   }
171 
172   // If the scanner is not idle, it is starting/stopping, and the appropriate
173   // scanning will be initiated in OnScanStatus().
174   if (scanner_->IsIdle()) {
175     StartScan(active);
176   }
177 }
178 
179 LowEnergyDiscoveryManager::PauseToken
PauseDiscovery()180 LowEnergyDiscoveryManager::PauseDiscovery() {
181   if (!paused()) {
182     bt_log(TRACE, "gap-le", "Pausing discovery");
183     StopScan();
184   }
185 
186   paused_count_.Set(*paused_count_ + 1);
187 
188   return PauseToken([this, self = GetWeakPtr()]() {
189     if (!self.is_alive()) {
190       return;
191     }
192 
193     PW_CHECK(paused());
194     paused_count_.Set(*paused_count_ - 1);
195     if (*paused_count_ == 0) {
196       ResumeDiscovery();
197     }
198   });
199 }
200 
discovering() const201 bool LowEnergyDiscoveryManager::discovering() const {
202   return std::any_of(
203       sessions_.begin(), sessions_.end(), [](auto& s) { return s->active(); });
204 }
205 
AttachInspect(inspect::Node & parent,std::string name)206 void LowEnergyDiscoveryManager::AttachInspect(inspect::Node& parent,
207                                               std::string name) {
208   inspect_.node = parent.CreateChild(name);
209   paused_count_.AttachInspect(inspect_.node, kInspectPausedCountPropertyName);
210   state_.AttachInspect(inspect_.node, kInspectStatePropertyName);
211   inspect_.failed_count =
212       inspect_.node.CreateUint(kInspectFailedCountPropertyName, 0);
213   inspect_.scan_interval_ms =
214       inspect_.node.CreateDouble(kInspectScanIntervalPropertyName, 0);
215   inspect_.scan_window_ms =
216       inspect_.node.CreateDouble(kInspectScanWindowPropertyName, 0);
217 }
218 
StateToString(State state)219 std::string LowEnergyDiscoveryManager::StateToString(State state) {
220   switch (state) {
221     case State::kIdle:
222       return "Idle";
223     case State::kStarting:
224       return "Starting";
225     case State::kActive:
226       return "Active";
227     case State::kPassive:
228       return "Passive";
229     case State::kStopping:
230       return "Stopping";
231   }
232 }
233 
234 std::unique_ptr<LowEnergyDiscoverySession>
AddSession(bool active)235 LowEnergyDiscoveryManager::AddSession(bool active) {
236   // Cannot use make_unique here since LowEnergyDiscoverySession has a private
237   // constructor.
238   std::unique_ptr<LowEnergyDiscoverySession> session(
239       new LowEnergyDiscoverySession(active, GetWeakPtr()));
240   sessions_.push_back(session.get());
241   return session;
242 }
243 
RemoveSession(LowEnergyDiscoverySession * session)244 void LowEnergyDiscoveryManager::RemoveSession(
245     LowEnergyDiscoverySession* session) {
246   PW_CHECK(session);
247 
248   // Only alive sessions are allowed to call this method. If there is at least
249   // one alive session object out there, then we MUST be scanning.
250   PW_CHECK(session->alive());
251 
252   auto iter = std::find(sessions_.begin(), sessions_.end(), session);
253   PW_CHECK(iter != sessions_.end());
254 
255   bool active = session->active();
256 
257   sessions_.erase(iter);
258 
259   bool last_active = active && std::none_of(sessions_.begin(),
260                                             sessions_.end(),
261                                             [](auto& s) { return s->active_; });
262 
263   // Stop scanning if the session count has dropped to zero or the scan type
264   // needs to be downgraded to passive.
265   if (sessions_.empty() || last_active) {
266     bt_log(TRACE,
267            "gap-le",
268            "Last %sdiscovery session removed, stopping scan (sessions: %zu)",
269            last_active ? "active " : "",
270            sessions_.size());
271     StopScan();
272     return;
273   }
274 }
275 
OnPeerFound(const hci::LowEnergyScanResult & result)276 void LowEnergyDiscoveryManager::OnPeerFound(
277     const hci::LowEnergyScanResult& result) {
278   bt_log(DEBUG,
279          "gap-le",
280          "peer found (address: %s, connectable: %d)",
281          bt_str(result.address()),
282          result.connectable());
283 
284   auto peer = peer_cache_->FindByAddress(result.address());
285   if (peer && peer->connectable() && peer->le() && connectable_cb_) {
286     bt_log(TRACE,
287            "gap-le",
288            "found connectable peer (id: %s)",
289            bt_str(peer->identifier()));
290     connectable_cb_(peer);
291   }
292 
293   // Don't notify sessions of unknown LE peers during passive scan.
294   if (scanner_->IsPassiveScanning() && (!peer || !peer->le())) {
295     return;
296   }
297 
298   // Create a new entry if we found the device during general discovery.
299   if (!peer) {
300     peer = peer_cache_->NewPeer(result.address(), result.connectable());
301     PW_CHECK(peer);
302   } else if (!peer->connectable() && result.connectable()) {
303     bt_log(DEBUG,
304            "gap-le",
305            "received connectable advertisement from previously non-connectable "
306            "peer (address: %s, "
307            "peer: %s)",
308            bt_str(result.address()),
309            bt_str(peer->identifier()));
310     peer->set_connectable(true);
311   }
312 
313   peer->MutLe().SetAdvertisingData(
314       result.rssi(), result.data(), dispatcher_.now());
315 
316   cached_scan_results_.insert(peer->identifier());
317 
318   for (auto iter = sessions_.begin(); iter != sessions_.end();) {
319     // The session may be erased by the result handler, so we need to get the
320     // next iterator before iter is invalidated.
321     auto next = std::next(iter);
322     auto session = *iter;
323     session->NotifyDiscoveryResult(*peer);
324     iter = next;
325   }
326 }
327 
OnDirectedAdvertisement(const hci::LowEnergyScanResult & result)328 void LowEnergyDiscoveryManager::OnDirectedAdvertisement(
329     const hci::LowEnergyScanResult& result) {
330   bt_log(TRACE,
331          "gap-le",
332          "Received directed advertisement (address: %s, %s)",
333          result.address().ToString().c_str(),
334          (result.resolved() ? "resolved" : "not resolved"));
335 
336   auto peer = peer_cache_->FindByAddress(result.address());
337   if (!peer) {
338     bt_log(DEBUG,
339            "gap-le",
340            "ignoring connection request from unknown peripheral: %s",
341            result.address().ToString().c_str());
342     return;
343   }
344 
345   if (!peer->le()) {
346     bt_log(DEBUG,
347            "gap-le",
348            "rejecting connection request from non-LE peripheral: %s",
349            result.address().ToString().c_str());
350     return;
351   }
352 
353   if (peer->connectable() && connectable_cb_) {
354     connectable_cb_(peer);
355   }
356 
357   // Only notify passive sessions.
358   for (auto iter = sessions_.begin(); iter != sessions_.end();) {
359     // The session may be erased by the result handler, so we need to get the
360     // next iterator before iter is invalidated.
361     auto next = std::next(iter);
362     auto session = *iter;
363     if (!session->active()) {
364       session->NotifyDiscoveryResult(*peer);
365     }
366     iter = next;
367   }
368 }
369 
OnScanStatus(hci::LowEnergyScanner::ScanStatus status)370 void LowEnergyDiscoveryManager::OnScanStatus(
371     hci::LowEnergyScanner::ScanStatus status) {
372   switch (status) {
373     case hci::LowEnergyScanner::ScanStatus::kFailed:
374       OnScanFailed();
375       return;
376     case hci::LowEnergyScanner::ScanStatus::kPassive:
377       OnPassiveScanStarted();
378       return;
379     case hci::LowEnergyScanner::ScanStatus::kActive:
380       OnActiveScanStarted();
381       return;
382     case hci::LowEnergyScanner::ScanStatus::kStopped:
383       OnScanStopped();
384       return;
385     case hci::LowEnergyScanner::ScanStatus::kComplete:
386       OnScanComplete();
387       return;
388   }
389 }
390 
OnScanFailed()391 void LowEnergyDiscoveryManager::OnScanFailed() {
392   bt_log(ERROR, "gap-le", "failed to initiate scan!");
393 
394   inspect_.failed_count.Add(1);
395   DeactivateAndNotifySessions();
396 
397   // Report failure on all currently pending requests. If any of the
398   // callbacks issue a retry the new requests will get re-queued and
399   // notified of failure in the same loop here.
400   while (!pending_.empty()) {
401     auto request = std::move(pending_.back());
402     pending_.pop_back();
403     request.callback(nullptr);
404   }
405 
406   state_.Set(State::kIdle);
407 }
408 
OnPassiveScanStarted()409 void LowEnergyDiscoveryManager::OnPassiveScanStarted() {
410   bt_log(TRACE, "gap-le", "passive scan started");
411 
412   state_.Set(State::kPassive);
413 
414   // Stop the passive scan if an active scan was requested while the scan was
415   // starting. The active scan will start in OnScanStopped() once the passive
416   // scan stops.
417   if (std::any_of(sessions_.begin(),
418                   sessions_.end(),
419                   [](auto& s) { return s->active_; }) ||
420       std::any_of(
421           pending_.begin(), pending_.end(), [](auto& p) { return p.active; })) {
422     bt_log(TRACE,
423            "gap-le",
424            "active scan requested while passive scan was starting");
425     StopScan();
426     return;
427   }
428 
429   NotifyPending();
430 }
431 
OnActiveScanStarted()432 void LowEnergyDiscoveryManager::OnActiveScanStarted() {
433   bt_log(TRACE, "gap-le", "active scan started");
434   state_.Set(State::kActive);
435   NotifyPending();
436 }
437 
OnScanStopped()438 void LowEnergyDiscoveryManager::OnScanStopped() {
439   bt_log(DEBUG,
440          "gap-le",
441          "stopped scanning (paused: %d, pending: %zu, sessions: %zu)",
442          paused(),
443          pending_.size(),
444          sessions_.size());
445 
446   state_.Set(State::kIdle);
447   cached_scan_results_.clear();
448 
449   if (paused()) {
450     return;
451   }
452 
453   if (!sessions_.empty()) {
454     bt_log(DEBUG, "gap-le", "initiating scanning");
455     bool active = std::any_of(
456         sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; });
457     StartScan(active);
458     return;
459   }
460 
461   // Some clients might have requested to start scanning while we were
462   // waiting for it to stop. Restart scanning if that is the case.
463   if (!pending_.empty()) {
464     bt_log(DEBUG, "gap-le", "initiating scanning");
465     bool active = std::any_of(
466         pending_.begin(), pending_.end(), [](auto& p) { return p.active; });
467     StartScan(active);
468     return;
469   }
470 }
471 
OnScanComplete()472 void LowEnergyDiscoveryManager::OnScanComplete() {
473   bt_log(TRACE, "gap-le", "end of scan period");
474 
475   state_.Set(State::kIdle);
476   cached_scan_results_.clear();
477 
478   if (paused()) {
479     return;
480   }
481 
482   // If |sessions_| is empty this is because sessions were stopped while the
483   // scanner was shutting down after the end of the scan period. Restart the
484   // scan as long as clients are waiting for it.
485   ResumeDiscovery();
486 }
487 
NotifyPending()488 void LowEnergyDiscoveryManager::NotifyPending() {
489   // Create and register all sessions before notifying the clients. We do
490   // this so that the reference count is incremented for all new sessions
491   // before the callbacks execute, to prevent a potential case in which a
492   // callback stops its session immediately which could cause the reference
493   // count to drop the zero before all clients receive their session object.
494   if (!pending_.empty()) {
495     size_t count = pending_.size();
496     std::vector<std::unique_ptr<LowEnergyDiscoverySession>> new_sessions(count);
497     std::generate(new_sessions.begin(),
498                   new_sessions.end(),
499                   [this, i = size_t{0}]() mutable {
500                     return AddSession(pending_[i++].active);
501                   });
502 
503     for (size_t i = count - 1; i < count; i--) {
504       auto cb = std::move(pending_.back().callback);
505       pending_.pop_back();
506       cb(std::move(new_sessions[i]));
507     }
508   }
509   PW_CHECK(pending_.empty());
510 }
511 
StartScan(bool active)512 void LowEnergyDiscoveryManager::StartScan(bool active) {
513   auto cb = [self = GetWeakPtr()](auto status) {
514     if (self.is_alive())
515       self->OnScanStatus(status);
516   };
517 
518   // TODO(armansito): A client that is interested in scanning nearby beacons and
519   // calculating proximity based on RSSI changes may want to disable duplicate
520   // filtering. We generally shouldn't allow this unless a client has the
521   // capability for it. Processing all HCI events containing advertising reports
522   // will both generate a lot of bus traffic and performing duplicate filtering
523   // on the host will take away CPU cycles from other things. It's a valid use
524   // case but needs proper management. For now we always make the controller
525   // filter duplicate reports.
526   hci::LowEnergyScanner::ScanOptions options{
527       .active = active,
528       .filter_duplicates = true,
529       .filter_policy =
530           pw::bluetooth::emboss::LEScanFilterPolicy::BASIC_UNFILTERED,
531       .period = scan_period_,
532       .scan_response_timeout = kLEScanResponseTimeout,
533   };
534 
535   // See Vol 3, Part C, 9.3.11 "Connection Establishment Timing Parameters".
536   if (active) {
537     options.interval = kLEActiveScanInterval;
538     options.window = kLEActiveScanWindow;
539   } else {
540     options.interval = kLEPassiveScanInterval;
541     options.window = kLEPassiveScanWindow;
542     // TODO(armansito): Use the controller filter accept policy to filter
543     // advertisements.
544   }
545 
546   // Since we use duplicate filtering, we stop and start the scan periodically
547   // to re-process advertisements. We use the minimum required scan period for
548   // general discovery (by default; |scan_period_| can be modified, e.g. by unit
549   // tests).
550   state_.Set(State::kStarting);
551   scanner_->StartScan(options, std::move(cb));
552 
553   inspect_.scan_interval_ms.Set(HciScanIntervalToMs(options.interval));
554   inspect_.scan_window_ms.Set(HciScanWindowToMs(options.window));
555 }
556 
StopScan()557 void LowEnergyDiscoveryManager::StopScan() {
558   state_.Set(State::kStopping);
559   scanner_->StopScan();
560 }
561 
ResumeDiscovery()562 void LowEnergyDiscoveryManager::ResumeDiscovery() {
563   PW_CHECK(!paused());
564 
565   if (!scanner_->IsIdle()) {
566     bt_log(TRACE, "gap-le", "attempt to resume discovery when it is not idle");
567     return;
568   }
569 
570   if (!sessions_.empty()) {
571     bt_log(TRACE, "gap-le", "resuming scan");
572     bool active = std::any_of(
573         sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; });
574     StartScan(active);
575     return;
576   }
577 
578   if (!pending_.empty()) {
579     bt_log(TRACE, "gap-le", "starting scan");
580     bool active = std::any_of(
581         pending_.begin(), pending_.end(), [](auto& s) { return s.active; });
582     StartScan(active);
583     return;
584   }
585 }
586 
DeactivateAndNotifySessions()587 void LowEnergyDiscoveryManager::DeactivateAndNotifySessions() {
588   // If there are any active sessions we invalidate by notifying of an error.
589 
590   // We move the initial set and notify those, if any error callbacks create
591   // additional sessions they will be added to pending_
592   auto sessions = std::move(sessions_);
593   for (const auto& session : sessions) {
594     if (session->alive()) {
595       session->NotifyError();
596     }
597   }
598 
599   // Due to the move, sessions_ should be empty before the loop and any
600   // callbacks will add sessions to pending_ so it should be empty
601   // afterwards as well.
602   PW_CHECK(sessions_.empty());
603 }
604 
605 }  // namespace bt::gap
606