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