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