• 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 #pragma once
16 #include <lib/fit/defer.h>
17 #include <pw_async/heap_dispatcher.h>
18 
19 #include <memory>
20 #include <unordered_set>
21 
22 #include "pw_bluetooth_sapphire/internal/host/common/inspectable.h"
23 #include "pw_bluetooth_sapphire/internal/host/common/weak_self.h"
24 #include "pw_bluetooth_sapphire/internal/host/gap/gap.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 {
29 
30 namespace hci {
31 class LowEnergyScanner;
32 class Transport;
33 }  // namespace hci
34 
35 namespace gap {
36 
37 class Peer;
38 class PeerCache;
39 
40 // LowEnergyDiscoveryManager implements GAP LE central/observer role
41 // discovery procedures. This class provides mechanisms for multiple clients to
42 // simultaneously scan for nearby peers filtered by adveritising data
43 // contents. This class also provides hooks for other layers to manage the
44 // Adapter's scan state for other procedures that require it (e.g. connection
45 // establishment, pairing procedures, and other scan and advertising
46 // procedures).
47 //
48 // An instance of LowEnergyDiscoveryManager can be initialized in either
49 // "legacy" or "extended" mode. The legacy mode is intended for Bluetooth
50 // controllers that only support the pre-5.0 HCI scan command set. The extended
51 // mode is intended for Bluetooth controllers that claim to support the "LE
52 // Extended Advertising" feature.
53 //
54 // Only one instance of LowEnergyDiscoveryManager should be created per
55 // hci::Transport object as multiple instances cannot correctly maintain state
56 // if they operate concurrently.
57 //
58 // To request a session, a client calls StartDiscovery() and asynchronously
59 // obtains a LowEnergyDiscoverySession that it uniquely owns. The session object
60 // can be configured with a callback to receive scan results. The session
61 // maintains an internal filter that may be modified to restrict the scan
62 // results based on properties of received advertisements.
63 //
64 // PROCEDURE:
65 //
66 // Starting the first discovery session initiates a periodic scan procedure, in
67 // which the scan is stopped and restarted for a given scan period (10.24
68 // seconds by default). This continues until all sessions have been removed.
69 //
70 // By default duplicate filtering is used which means that a new advertising
71 // report will be generated for each discovered advertiser only once per scan
72 // period. Scan results for each scan period are cached so that sessions added
73 // during a scan period can receive previously processed results.
74 //
75 // EXAMPLE:
76 //     bt::gap::LowEnergyDiscoveryManager discovery_manager(
77 //         bt::gap::LowEnergyDiscoveryManager::Mode::kLegacy,
78 //         transport, dispatcher);
79 //     ...
80 //
81 //     // Only scan for peers advertising the "Heart Rate" GATT Service.
82 //     uint16_t uuid = 0x180d;
83 //     bt::hci::DiscoveryFilter discovery_filter;
84 //     discovery_filter.set_service_uuids({bt::UUID(uuid)});
85 //
86 //     std::vector<bt::hci::DiscoveryFilter> discovery_filters;
87 //     discovery_filters.push_back(discovery_filter);
88 //
89 //     std::unique_ptr<bt::gap::LowEnergyDiscoverySession> session;
90 //     discovery_manager.StartDiscovery(/*active=*/true, discovery_filters,
91 //       [&session](auto new_session) {
92 //         // Take ownership of the session to make sure it isn't terminated
93 //         // when this callback returns.
94 //         session = std::move(new_session);
95 //
96 //         session->SetResultCallback([](
97 //           const bt::hci::LowEnergyScanResult& result,
98 //           const bt::ByteBuffer& advertising_data) {
99 //             // Do stuff with |result| and |advertising_data|.
100 //             // (|advertising_data| contains any received Scan Response data
101 //             // as well).
102 //           });
103 //       });
104 //
105 // NOTE: These classes are not thread-safe. An instance of
106 // LowEnergyDiscoveryManager is bound to its creation thread and the associated
107 // dispatcher and must be accessed and destroyed on the same thread.
108 
109 // Represents a LE discovery session initiated via
110 // LowEnergyDiscoveryManager::StartDiscovery(). Instances cannot be created
111 // directly; instead they are handed to callers by LowEnergyDiscoveryManager.
112 //
113 // The discovery classes are not thread-safe. A LowEnergyDiscoverySession MUST
114 // be accessed and destroyed on the thread that it was created on.
115 
116 class LowEnergyDiscoverySession;
117 using LowEnergyDiscoverySessionPtr = std::unique_ptr<LowEnergyDiscoverySession>;
118 
119 // See comments above.
120 class LowEnergyDiscoveryManager final
121     : public hci::LowEnergyScanner::Delegate,
122       public WeakSelf<LowEnergyDiscoveryManager> {
123  public:
124   // |peer_cache| and |scanner| MUST out-live this LowEnergyDiscoveryManager.
125   LowEnergyDiscoveryManager(
126       hci::LowEnergyScanner* scanner,
127       PeerCache* peer_cache,
128       const hci::LowEnergyScanner::PacketFilterConfig& packet_filter_config,
129       pw::async::Dispatcher& dispatcher);
130   ~LowEnergyDiscoveryManager() override;
131 
132   // Starts a new discovery session and reports the result via |callback|. If a
133   // session has been successfully started the caller will receive a new
134   // LowEnergyDiscoverySession instance via |callback| which it uniquely owns.
135   // |active| indicates whether active or passive discovery should occur.
136   // On failure a nullptr will be returned via |callback|.
137   //
138   // TODO(armansito): Implement option to disable duplicate filtering. Would
139   // this require software filtering for clients that did not request it?
140   using SessionCallback = fit::function<void(LowEnergyDiscoverySessionPtr)>;
141   void StartDiscovery(bool active,
142                       std::vector<hci::DiscoveryFilter> filters,
143                       SessionCallback callback);
144 
145   // Pause current and future discovery sessions until the returned PauseToken
146   // is destroyed. If PauseDiscovery is called multiple times, discovery will be
147   // paused until all returned PauseTokens are destroyed. NOTE:
148   // deferred_action::cancel() must not be called, or else discovery will never
149   // resume.
150   using PauseToken = fit::deferred_action<fit::callback<void()>>;
151   [[nodiscard]] PauseToken PauseDiscovery();
152 
153   // Sets a new scan period to any future and ongoing discovery procedures.
set_scan_period(pw::chrono::SystemClock::duration period)154   void set_scan_period(pw::chrono::SystemClock::duration period) {
155     scan_period_ = period;
156   }
157 
158   // Returns whether there is an active scan in progress.
159   bool discovering() const;
160 
161   // Returns true if discovery is paused.
paused()162   bool paused() const { return *paused_count_ != 0; }
163 
164   // Registers a callback which runs when a connectable advertisement is
165   // received from known peer which was previously observed to be connectable
166   // during general discovery. The |peer| argument is guaranteed to be valid
167   // until the callback returns. The callback can also assume that LE transport
168   // information (i.e. |peer->le()|) will be present and accessible.
169   using PeerConnectableCallback = fit::function<void(Peer* peer)>;
set_peer_connectable_callback(PeerConnectableCallback callback)170   void set_peer_connectable_callback(PeerConnectableCallback callback) {
171     connectable_cb_ = std::move(callback);
172   }
173 
174   void AttachInspect(inspect::Node& parent, std::string name);
175 
176  private:
177   enum class State {
178     kIdle,
179     kStarting,
180     kActive,
181     kPassive,
182     kStopping,
183   };
184   static std::string StateToString(State state);
185 
186   struct InspectProperties {
187     inspect::Node node;
188     inspect::UintProperty failed_count;
189     inspect::DoubleProperty scan_interval_ms;
190     inspect::DoubleProperty scan_window_ms;
191   };
192 
peer_cache()193   const PeerCache* peer_cache() const { return peer_cache_; }
194 
cached_scan_results()195   const std::unordered_set<PeerId>& cached_scan_results() const {
196     return cached_scan_results_;
197   }
198 
199   // Creates and stores a new session object and returns it.
200   std::unique_ptr<LowEnergyDiscoverySession> AddSession(
201       bool active, std::vector<hci::DiscoveryFilter> discovery_filters);
202 
203   // Called by LowEnergyDiscoverySession to stop a session that it was assigned
204   // to.
205   void RemoveSession(LowEnergyDiscoverySession* session);
206 
207   // hci::LowEnergyScanner::Delegate override:
208   void OnPeerFound(const hci::LowEnergyScanResult& result) override;
209   void OnDirectedAdvertisement(const hci::LowEnergyScanResult& result) override;
210 
211   // Called by hci::LowEnergyScanner
212   void OnScanStatus(hci::LowEnergyScanner::ScanStatus status);
213 
214   // Handlers for scan status updates.
215   void OnScanFailed();
216   void OnPassiveScanStarted();
217   void OnActiveScanStarted();
218   void OnScanStopped();
219   void OnScanComplete();
220 
221   // Create sessions for all pending requests and pass the sessions to the
222   // request callbacks.
223   void NotifyPending();
224 
225   // Tells the scanner to start scanning. Aliases are provided for improved
226   // readability.
227   void StartScan(bool active);
StartActiveScan()228   inline void StartActiveScan() { StartScan(true); }
StartPassiveScan()229   inline void StartPassiveScan() { StartScan(false); }
230 
231   // Tells the scanner to stop scanning.
232   void StopScan();
233 
234   // If there are any pending requests or valid sessions, start discovery.
235   // Discovery must not be paused.
236   // Called when discovery is unpaused or the scan period ends and needs to be
237   // restarted.
238   void ResumeDiscovery();
239 
240   // Used by destructor to handle all sessions
241   void DeactivateAndNotifySessions();
242 
243   // The dispatcher that we use for invoking callbacks asynchronously.
244   pw::async::Dispatcher& dispatcher_;
245   pw::async::HeapDispatcher heap_dispatcher_{dispatcher_};
246 
247   InspectProperties inspect_;
248 
249   StringInspectable<State> state_;
250 
251   // The peer cache that we use for storing and looking up scan results. We
252   // hold a raw pointer as we expect this to out-live us.
253   PeerCache* const peer_cache_;
254 
255   uint16_t next_scan_id_ = 0;
256   hci::LowEnergyScanner::PacketFilterConfig packet_filter_config_;
257 
258   // Called when a directed connectable advertisement is received during an
259   // active or passive scan.
260   PeerConnectableCallback connectable_cb_;
261 
262   // The list of currently pending calls to start discovery.
263   struct DiscoveryRequest {
264     bool active;
265     std::vector<hci::DiscoveryFilter> filters;
266     SessionCallback callback;
267   };
268   std::vector<DiscoveryRequest> pending_;
269 
270   // The list of currently active/known sessions. We store raw (weak) pointers
271   // here because, while we don't actually own the session objects they will
272   // always notify us before destruction so we can remove them from this list.
273   //
274   // The number of elements in |sessions_| acts as our scan reference count.
275   // When |sessions_| becomes empty scanning is stopped. Similarly, scanning is
276   // started on the insertion of the first element.
277   std::list<LowEnergyDiscoverySession*> sessions_;
278 
279   // Identifiers for the cached scan results for the current scan period during
280   // discovery. The minimum (and default) scan period is 10.24 seconds
281   // when performing LE discovery. This can cause a long wait for a discovery
282   // session that joined in the middle of a scan period and duplicate filtering
283   // is enabled. We maintain this cache to immediately notify new sessions of
284   // the currently cached results for this period.
285   std::unordered_set<PeerId> cached_scan_results_;
286 
287   // The value (in ms) that we use for the duration of each scan period.
288   pw::chrono::SystemClock::duration scan_period_ = kLEGeneralDiscoveryScanMin;
289 
290   // Count of the number of outstanding PauseTokens. When |paused_count_| is 0,
291   // discovery is unpaused.
292   IntInspectable<int> paused_count_;
293 
294   // The scanner that performs the HCI procedures. |scanner_| must out-live this
295   // discovery manager.
296   hci::LowEnergyScanner* scanner_;  // weak
297 
298   BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyDiscoveryManager);
299 };
300 
301 class LowEnergyDiscoverySession final
302     : public WeakSelf<LowEnergyDiscoverySession> {
303  public:
304   LowEnergyDiscoverySession(
305       uint16_t scan_id,
306       bool active,
307       std::vector<hci::DiscoveryFilter> filters,
308       PeerCache& peer_cache,
309       pw::async::Dispatcher& dispatcher,
310       fit::function<void(LowEnergyDiscoverySession*)> on_stop_cb,
311       fit::function<const std::unordered_set<PeerId>&()>
312           cached_scan_results_fn);
313 
314   // Destroying a session instance automatically ends the session.
315   ~LowEnergyDiscoverySession();
316 
317   // Sets a callback for receiving notifications on discovered peers.
318   // |data| contains advertising and scan response data (if any) obtained during
319   // discovery.
320   //
321   // When this callback is set, it will immediately receive notifications for
322   // the cached results from the most recent scan period. If a filter was
323   // assigned earlier, then the callback will only receive results that match
324   // the filter.
325   //
326   // Passive discovery sessions will call this callback for both directed and
327   // undirected advertisements from known peers, while active discovery sessions
328   // will ignore directed advertisements (as they are not from new peers).
329   using PeerFoundFunction = fit::function<void(const Peer& peer)>;
330   void SetResultCallback(PeerFoundFunction callback);
331 
332   // Called to deliver scan results.
333   void NotifyDiscoveryResult(const Peer& peer) const;
334 
335   // Sets a callback to get notified when the session becomes inactive due to an
336   // internal error.
set_error_callback(fit::closure callback)337   void set_error_callback(fit::closure callback) {
338     error_cb_ = std::move(callback);
339   }
340 
341   // Marks this session as inactive and notifies the error handler.
342   void NotifyError();
343 
344   // Ends this session. This instance will stop receiving notifications for
345   // peers.
346   void Stop();
347 
348   // Returns true if this session has not been stopped and has not errored.
alive()349   bool alive() const { return alive_; }
350 
scan_id()351   uint16_t scan_id() const { return scan_id_; }
352 
353   // Returns true if this is an active discovery session, or false if this is a
354   // passive discovery session.
active()355   bool active() const { return active_; }
356 
357  private:
358   uint16_t scan_id_;
359   bool alive_ = true;
360   bool active_;
361   std::vector<hci::DiscoveryFilter> filters_;
362   PeerCache& peer_cache_;
363   pw::async::HeapDispatcher heap_dispatcher_;
364   fit::callback<void()> error_cb_;
365   PeerFoundFunction peer_found_fn_;
366   fit::callback<void(LowEnergyDiscoverySession*)> on_stop_cb_;
367   fit::function<const std::unordered_set<PeerId>&()> cached_scan_results_fn_;
368 
369   BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyDiscoverySession);
370 };
371 
372 }  // namespace gap
373 }  // namespace bt
374