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