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