• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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   BT_ASSERT(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   BT_DEBUG_ASSERT(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   BT_ASSERT(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   BT_DEBUG_ASSERT(peer_cache_);
107   BT_DEBUG_ASSERT(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   BT_ASSERT(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     BT_ASSERT(!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         [callback = std::move(callback), session = std::move(session)](
158             pw::async::Context /*ctx*/, pw::Status status) mutable {
159           if (status.ok()) {
160             callback(std::move(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     BT_ASSERT(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   BT_ASSERT(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   BT_ASSERT(session->alive());
251 
252   auto iter = std::find(sessions_.begin(), sessions_.end(), session);
253   BT_ASSERT(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,const ByteBuffer & data)276 void LowEnergyDiscoveryManager::OnPeerFound(
277     const hci::LowEnergyScanResult& result, const ByteBuffer& data) {
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     BT_ASSERT(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(result.rssi, data, dispatcher_.now());
314 
315   cached_scan_results_.insert(peer->identifier());
316 
317   for (auto iter = sessions_.begin(); iter != sessions_.end();) {
318     // The session may be erased by the result handler, so we need to get the
319     // next iterator before iter is invalidated.
320     auto next = std::next(iter);
321     auto session = *iter;
322     session->NotifyDiscoveryResult(*peer);
323     iter = next;
324   }
325 }
326 
OnDirectedAdvertisement(const hci::LowEnergyScanResult & result)327 void LowEnergyDiscoveryManager::OnDirectedAdvertisement(
328     const hci::LowEnergyScanResult& result) {
329   bt_log(TRACE,
330          "gap-le",
331          "Received directed advertisement (address: %s, %s)",
332          result.address.ToString().c_str(),
333          (result.resolved ? "resolved" : "not resolved"));
334 
335   auto peer = peer_cache_->FindByAddress(result.address);
336   if (!peer) {
337     bt_log(DEBUG,
338            "gap-le",
339            "ignoring connection request from unknown peripheral: %s",
340            result.address.ToString().c_str());
341     return;
342   }
343 
344   if (!peer->le()) {
345     bt_log(DEBUG,
346            "gap-le",
347            "rejecting connection request from non-LE peripheral: %s",
348            result.address.ToString().c_str());
349     return;
350   }
351 
352   if (peer->connectable() && connectable_cb_) {
353     connectable_cb_(peer);
354   }
355 
356   // Only notify passive sessions.
357   for (auto iter = sessions_.begin(); iter != sessions_.end();) {
358     // The session may be erased by the result handler, so we need to get the
359     // next iterator before iter is invalidated.
360     auto next = std::next(iter);
361     auto session = *iter;
362     if (!session->active()) {
363       session->NotifyDiscoveryResult(*peer);
364     }
365     iter = next;
366   }
367 }
368 
OnScanStatus(hci::LowEnergyScanner::ScanStatus status)369 void LowEnergyDiscoveryManager::OnScanStatus(
370     hci::LowEnergyScanner::ScanStatus status) {
371   switch (status) {
372     case hci::LowEnergyScanner::ScanStatus::kFailed:
373       OnScanFailed();
374       return;
375     case hci::LowEnergyScanner::ScanStatus::kPassive:
376       OnPassiveScanStarted();
377       return;
378     case hci::LowEnergyScanner::ScanStatus::kActive:
379       OnActiveScanStarted();
380       return;
381     case hci::LowEnergyScanner::ScanStatus::kStopped:
382       OnScanStopped();
383       return;
384     case hci::LowEnergyScanner::ScanStatus::kComplete:
385       OnScanComplete();
386       return;
387   }
388 }
389 
OnScanFailed()390 void LowEnergyDiscoveryManager::OnScanFailed() {
391   bt_log(ERROR, "gap-le", "failed to initiate scan!");
392 
393   inspect_.failed_count.Add(1);
394   DeactivateAndNotifySessions();
395 
396   // Report failure on all currently pending requests. If any of the
397   // callbacks issue a retry the new requests will get re-queued and
398   // notified of failure in the same loop here.
399   while (!pending_.empty()) {
400     auto request = std::move(pending_.back());
401     pending_.pop_back();
402     request.callback(nullptr);
403   }
404 
405   state_.Set(State::kIdle);
406 }
407 
OnPassiveScanStarted()408 void LowEnergyDiscoveryManager::OnPassiveScanStarted() {
409   bt_log(TRACE, "gap-le", "passive scan started");
410 
411   state_.Set(State::kPassive);
412 
413   // Stop the passive scan if an active scan was requested while the scan was
414   // starting. The active scan will start in OnScanStopped() once the passive
415   // scan stops.
416   if (std::any_of(sessions_.begin(),
417                   sessions_.end(),
418                   [](auto& s) { return s->active_; }) ||
419       std::any_of(
420           pending_.begin(), pending_.end(), [](auto& p) { return p.active; })) {
421     bt_log(TRACE,
422            "gap-le",
423            "active scan requested while passive scan was starting");
424     StopScan();
425     return;
426   }
427 
428   NotifyPending();
429 }
430 
OnActiveScanStarted()431 void LowEnergyDiscoveryManager::OnActiveScanStarted() {
432   bt_log(TRACE, "gap-le", "active scan started");
433   state_.Set(State::kActive);
434   NotifyPending();
435 }
436 
OnScanStopped()437 void LowEnergyDiscoveryManager::OnScanStopped() {
438   bt_log(DEBUG,
439          "gap-le",
440          "stopped scanning (paused: %d, pending: %zu, sessions: %zu)",
441          paused(),
442          pending_.size(),
443          sessions_.size());
444 
445   state_.Set(State::kIdle);
446   cached_scan_results_.clear();
447 
448   if (paused()) {
449     return;
450   }
451 
452   if (!sessions_.empty()) {
453     bt_log(DEBUG, "gap-le", "initiating scanning");
454     bool active = std::any_of(
455         sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; });
456     StartScan(active);
457     return;
458   }
459 
460   // Some clients might have requested to start scanning while we were
461   // waiting for it to stop. Restart scanning if that is the case.
462   if (!pending_.empty()) {
463     bt_log(DEBUG, "gap-le", "initiating scanning");
464     bool active = std::any_of(
465         pending_.begin(), pending_.end(), [](auto& p) { return p.active; });
466     StartScan(active);
467     return;
468   }
469 }
470 
OnScanComplete()471 void LowEnergyDiscoveryManager::OnScanComplete() {
472   bt_log(TRACE, "gap-le", "end of scan period");
473 
474   state_.Set(State::kIdle);
475   cached_scan_results_.clear();
476 
477   if (paused()) {
478     return;
479   }
480 
481   // If |sessions_| is empty this is because sessions were stopped while the
482   // scanner was shutting down after the end of the scan period. Restart the
483   // scan as long as clients are waiting for it.
484   ResumeDiscovery();
485 }
486 
NotifyPending()487 void LowEnergyDiscoveryManager::NotifyPending() {
488   // Create and register all sessions before notifying the clients. We do
489   // this so that the reference count is incremented for all new sessions
490   // before the callbacks execute, to prevent a potential case in which a
491   // callback stops its session immediately which could cause the reference
492   // count to drop the zero before all clients receive their session object.
493   if (!pending_.empty()) {
494     size_t count = pending_.size();
495     std::vector<std::unique_ptr<LowEnergyDiscoverySession>> new_sessions(count);
496     std::generate(new_sessions.begin(),
497                   new_sessions.end(),
498                   [this, i = size_t{0}]() mutable {
499                     return AddSession(pending_[i++].active);
500                   });
501 
502     for (size_t i = count - 1; i < count; i--) {
503       auto cb = std::move(pending_.back().callback);
504       pending_.pop_back();
505       cb(std::move(new_sessions[i]));
506     }
507   }
508   BT_ASSERT(pending_.empty());
509 }
510 
StartScan(bool active)511 void LowEnergyDiscoveryManager::StartScan(bool active) {
512   auto cb = [self = GetWeakPtr()](auto status) {
513     if (self.is_alive())
514       self->OnScanStatus(status);
515   };
516 
517   // TODO(armansito): A client that is interested in scanning nearby beacons and
518   // calculating proximity based on RSSI changes may want to disable duplicate
519   // filtering. We generally shouldn't allow this unless a client has the
520   // capability for it. Processing all HCI events containing advertising reports
521   // will both generate a lot of bus traffic and performing duplicate filtering
522   // on the host will take away CPU cycles from other things. It's a valid use
523   // case but needs proper management. For now we always make the controller
524   // filter duplicate reports.
525   hci::LowEnergyScanner::ScanOptions options{
526       .active = active,
527       .filter_duplicates = true,
528       .filter_policy =
529           pw::bluetooth::emboss::LEScanFilterPolicy::BASIC_UNFILTERED,
530       .period = scan_period_,
531       .scan_response_timeout = kLEScanResponseTimeout,
532   };
533 
534   // See Vol 3, Part C, 9.3.11 "Connection Establishment Timing Parameters".
535   if (active) {
536     options.interval = kLEActiveScanInterval;
537     options.window = kLEActiveScanWindow;
538   } else {
539     options.interval = kLEPassiveScanInterval;
540     options.window = kLEPassiveScanWindow;
541     // TODO(armansito): Use the controller filter accept policy to filter
542     // advertisements.
543   }
544 
545   // Since we use duplicate filtering, we stop and start the scan periodically
546   // to re-process advertisements. We use the minimum required scan period for
547   // general discovery (by default; |scan_period_| can be modified, e.g. by unit
548   // tests).
549   state_.Set(State::kStarting);
550   scanner_->StartScan(options, std::move(cb));
551 
552   inspect_.scan_interval_ms.Set(HciScanIntervalToMs(options.interval));
553   inspect_.scan_window_ms.Set(HciScanWindowToMs(options.window));
554 }
555 
StopScan()556 void LowEnergyDiscoveryManager::StopScan() {
557   state_.Set(State::kStopping);
558   scanner_->StopScan();
559 }
560 
ResumeDiscovery()561 void LowEnergyDiscoveryManager::ResumeDiscovery() {
562   BT_ASSERT(!paused());
563 
564   if (!scanner_->IsIdle()) {
565     bt_log(TRACE, "gap-le", "attempt to resume discovery when it is not idle");
566     return;
567   }
568 
569   if (!sessions_.empty()) {
570     bt_log(TRACE, "gap-le", "resuming scan");
571     bool active = std::any_of(
572         sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; });
573     StartScan(active);
574     return;
575   }
576 
577   if (!pending_.empty()) {
578     bt_log(TRACE, "gap-le", "starting scan");
579     bool active = std::any_of(
580         pending_.begin(), pending_.end(), [](auto& s) { return s.active; });
581     StartScan(active);
582     return;
583   }
584 }
585 
DeactivateAndNotifySessions()586 void LowEnergyDiscoveryManager::DeactivateAndNotifySessions() {
587   // If there are any active sessions we invalidate by notifying of an error.
588 
589   // We move the initial set and notify those, if any error callbacks create
590   // additional sessions they will be added to pending_
591   auto sessions = std::move(sessions_);
592   for (const auto& session : sessions) {
593     if (session->alive()) {
594       session->NotifyError();
595     }
596   }
597 
598   // Due to the move, sessions_ should be empty before the loop and any
599   // callbacks will add sessions to pending_ so it should be empty
600   // afterwards as well.
601   BT_ASSERT(sessions_.empty());
602 }
603 
604 }  // namespace bt::gap
605