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