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