blob: 4a9da4225ad261375cfbec922bdb65754fb72fbd [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SRC_CONNECTIVITY_BLUETOOTH_CORE_BT_HOST_GAP_LOW_ENERGY_DISCOVERY_MANAGER_H_
#define SRC_CONNECTIVITY_BLUETOOTH_CORE_BT_HOST_GAP_LOW_ENERGY_DISCOVERY_MANAGER_H_
#include <lib/async/dispatcher.h>
#include <lib/fit/defer.h>
#include <lib/fit/thread_checker.h>
#include <lib/sys/inspect/cpp/component.h>
#include <memory>
#include <queue>
#include <unordered_set>
#include "src/connectivity/bluetooth/core/bt-host/common/byte_buffer.h"
#include "src/connectivity/bluetooth/core/bt-host/common/device_address.h"
#include "src/connectivity/bluetooth/core/bt-host/common/inspectable.h"
#include "src/connectivity/bluetooth/core/bt-host/gap/discovery_filter.h"
#include "src/connectivity/bluetooth/core/bt-host/gap/gap.h"
#include "src/connectivity/bluetooth/core/bt-host/hci/low_energy_scanner.h"
#include "src/lib/fxl/memory/ref_ptr.h"
#include "src/lib/fxl/memory/weak_ptr.h"
namespace bt {
namespace hci {
class LowEnergyScanner;
class Transport;
} // namespace hci
namespace gap {
class LowEnergyDiscoveryManager;
class Peer;
class PeerCache;
// LowEnergyDiscoveryManager implements GAP LE central/observer role
// discovery procedures. This class provides mechanisms for multiple clients to
// simultaneously scan for nearby peers filtered by adveritising data
// contents. This class also provides hooks for other layers to manage the
// Adapter's scan state for other procedures that require it (e.g. connection
// establishment, pairing procedures, and other scan and advertising
// procedures).
// TODO(armansito): The last sentence of this paragraph hasn't been implemented
// yet.
//
// An instance of LowEnergyDiscoveryManager can be initialized in either
// "legacy" or "extended" mode. The legacy mode is intended for Bluetooth
// controllers that only support the pre-5.0 HCI scan command set. The extended
// mode is intended for Bluetooth controllers that claim to support the "LE
// Extended Advertising" feature.
//
// Only one instance of LowEnergyDiscoveryManager should be created per
// hci::Transport object as multiple instances cannot correctly maintain state
// if they operate concurrently.
//
// To request a session, a client calls StartDiscovery() and asynchronously
// obtains a LowEnergyDiscoverySession that it uniquely owns. The session object
// can be configured with a callback to receive scan results. The session
// maintains an internal filter that may be modified to restrict the scan
// results based on properties of received advertisements.
//
// PROCEDURE:
//
// Starting the first discovery session initiates a periodic scan procedure, in
// which the scan is stopped and restarted for a given scan period (10.24
// seconds by default). This continues until all sessions have been removed.
//
// By default duplicate filtering is used which means that a new advertising
// report will be generated for each discovered advertiser only once per scan
// period. Scan results for each scan period are cached so that sessions added
// during a scan period can receive previously processed results.
//
// EXAMPLE:
// bt::gap::LowEnergyDiscoveryManager discovery_manager(
// bt::gap::LowEnergyDiscoveryManager::Mode::kLegacy,
// transport, dispatcher);
// ...
//
// std::unique_ptr<bt::gap::LowEnergyDiscoverySession> session;
// discovery_manager.StartDiscovery(/*active=*/true, [&session](auto new_session) {
// // Take ownership of the session to make sure it isn't terminated when
// // this callback returns.
// session = std::move(new_session);
//
// // Only scan for peers advertising the "Heart Rate" GATT Service.
// uint16_t uuid = 0x180d;
// session->filter()->set_service_uuids({bt::UUID(uuid)});
// session->SetResultCallback([](const
// bt::hci::LowEnergyScanResult& result,
// const bt::ByteBuffer&
// advertising_data) {
// // Do stuff with |result| and |advertising_data|. (|advertising_data|
// // contains any received Scan Response data as well).
// });
// });
//
// NOTE: These classes are not thread-safe. An instance of
// LowEnergyDiscoveryManager is bound to its creation thread and the associated
// dispatcher and must be accessed and destroyed on the same thread.
// Represents a LE discovery session initiated via
// LowEnergyDiscoveryManager::StartDiscovery(). Instances cannot be created
// directly; instead they are handed to callers by LowEnergyDiscoveryManager.
//
// The discovery classes are not thread-safe. A LowEnergyDiscoverySession MUST
// be accessed and destroyed on the thread that it was created on.
class LowEnergyDiscoverySession final {
public:
// Destroying a session instance automatically ends the session. To terminate
// a session, a client may either explicitly call Stop() or simply destroy
// this instance.
~LowEnergyDiscoverySession();
// Sets a callback for receiving notifications on discovered peers.
// |data| contains advertising and scan response data (if any) obtained during
// discovery.
//
// When this callback is set, it will immediately receive notifications for
// the cached results from the most recent scan period. If a filter was
// assigned earlier, then the callback will only receive results that match
// the filter.
//
// Passive discovery sessions will call this callback for both directed and undirected
// advertisements from known peers, while active discovery sessions will ignore directed
// advertisements (as they are not from new peers).
using PeerFoundCallback = fit::function<void(const Peer& peer)>;
void SetResultCallback(PeerFoundCallback callback);
// Sets a callback to get notified when the session becomes inactive due to an
// internal error.
void set_error_callback(fit::closure callback) { error_callback_ = std::move(callback); }
// Returns the filter that belongs to this session. The caller may modify the
// filter as desired. By default no peers are filtered.
//
// NOTE: The client is responsible for setting up the filter's "flags" field
// for discovery procedures.
DiscoveryFilter* filter() { return &filter_; }
// Ends this session. This instance will stop receiving notifications for
// peers.
void Stop();
// Returns true if this session has not been stopped and has not errored.
bool alive() const { return alive_; }
// Returns true if this is an active discovery session, or false if this is a passive discovery
// session.
bool active() const { return active_; }
private:
friend class LowEnergyDiscoveryManager;
// Called by LowEnergyDiscoveryManager.
explicit LowEnergyDiscoverySession(bool active, fxl::WeakPtr<LowEnergyDiscoveryManager> manager);
// Called by LowEnergyDiscoveryManager on newly discovered scan results.
void NotifyDiscoveryResult(const Peer& peer) const;
// Marks this session as inactive and notifies the error handler.
void NotifyError();
bool alive_;
bool active_;
fxl::WeakPtr<LowEnergyDiscoveryManager> manager_;
fit::closure error_callback_;
PeerFoundCallback peer_found_callback_;
DiscoveryFilter filter_;
fit::thread_checker thread_checker_;
DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyDiscoverySession);
};
using LowEnergyDiscoverySessionPtr = std::unique_ptr<LowEnergyDiscoverySession>;
// See comments above.
class LowEnergyDiscoveryManager final : public hci::LowEnergyScanner::Delegate {
public:
// |peer_cache| and |scanner| MUST out-live this LowEnergyDiscoveryManager.
LowEnergyDiscoveryManager(fxl::WeakPtr<hci::Transport> hci, hci::LowEnergyScanner* scanner,
PeerCache* peer_cache);
virtual ~LowEnergyDiscoveryManager();
// Starts a new discovery session and reports the result via |callback|. If a
// session has been successfully started the caller will receive a new
// LowEnergyDiscoverySession instance via |callback| which it uniquely owns.
// |active| indicates whether active or passive discovery should occur.
// On failure a nullptr will be returned via |callback|.
//
// TODO(armansito): Implement option to disable duplicate filtering. Would
// this require software filtering for clients that did not request it?
using SessionCallback = fit::function<void(LowEnergyDiscoverySessionPtr)>;
void StartDiscovery(bool active, SessionCallback callback);
// Pause current and future discovery sessions until the returned PauseToken is destroyed.
// If PauseDiscovery is called multiple times, discovery will be paused until all returned
// PauseTokens are destroyed.
// NOTE: deferred_action::cancel() must not be called, or else discovery will never resume.
using PauseToken = fit::deferred_action<fit::callback<void()>>;
[[nodiscard]] PauseToken PauseDiscovery();
// Sets a new scan period to any future and ongoing discovery procedures.
void set_scan_period(zx::duration period) { scan_period_ = period; }
// Returns whether there is an active scan in progress.
bool discovering() const;
// Returns true if discovery is paused.
bool paused() const { return *paused_count_ != 0; }
// Registers a callback which runs when a connectable advertisement is
// received from known peer which was previously observed to be connectable during general
// discovery. The |peer| argument is guaranteed to be valid until the callback returns.
// The callback can also assume that LE transport information (i.e. |peer->le()|) will be present
// and accessible.
using PeerConnectableCallback = fit::function<void(Peer* peer)>;
void set_peer_connectable_callback(PeerConnectableCallback callback) {
connectable_cb_ = std::move(callback);
}
void AttachInspect(inspect::Node& parent, std::string name);
fxl::WeakPtr<LowEnergyDiscoveryManager> GetWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); }
private:
friend class LowEnergyDiscoverySession;
enum class State {
kIdle,
kStarting,
kActive,
kPassive,
kStopping,
};
static std::string StateToString(State state);
struct InspectProperties {
inspect::Node node;
inspect::UintProperty failed_count;
inspect::DoubleProperty scan_interval_ms;
inspect::DoubleProperty scan_window_ms;
};
const PeerCache* peer_cache() const { return peer_cache_; }
const std::unordered_set<PeerId>& cached_scan_results() const { return cached_scan_results_; }
// Creates and stores a new session object and returns it.
std::unique_ptr<LowEnergyDiscoverySession> AddSession(bool active);
// Called by LowEnergyDiscoverySession to stop a session that it was assigned
// to.
void RemoveSession(LowEnergyDiscoverySession* session);
// hci::LowEnergyScanner::Delegate override:
void OnPeerFound(const hci::LowEnergyScanResult& result, const ByteBuffer& data) override;
void OnDirectedAdvertisement(const hci::LowEnergyScanResult& result) override;
// Called by hci::LowEnergyScanner
void OnScanStatus(hci::LowEnergyScanner::ScanStatus status);
// Handlers for scan status updates.
void OnScanFailed();
void OnPassiveScanStarted();
void OnActiveScanStarted();
void OnScanStopped();
void OnScanComplete();
// Create sessions for all pending requests and pass the sessions to the request callbacks.
void NotifyPending();
// Tells the scanner to start scanning. Aliases are provided for improved
// readability.
void StartScan(bool active);
inline void StartActiveScan() { StartScan(true); }
inline void StartPassiveScan() { StartScan(false); }
// Tells the scanner to stop scanning.
void StopScan();
// If there are any pending requests or valid sessions, start discovery.
// Discovery must not be paused.
// Called when discovery is unpaused or the scan period ends and needs to be restarted.
void ResumeDiscovery();
// Used by destructor to handle all sessions
void DeactivateAndNotifySessions();
// The dispatcher that we use for invoking callbacks asynchronously.
async_dispatcher_t* dispatcher_;
InspectProperties inspect_;
StringInspectable<State> state_;
// The peer cache that we use for storing and looking up scan results. We
// hold a raw pointer as we expect this to out-live us.
PeerCache* const peer_cache_;
// Called when a directed connectable advertisement is received during an
// active or passive scan.
PeerConnectableCallback connectable_cb_;
// The list of currently pending calls to start discovery.
struct DiscoveryRequest {
bool active;
SessionCallback callback;
};
std::vector<DiscoveryRequest> pending_;
// The list of currently active/known sessions. We store raw (weak) pointers
// here because, while we don't actually own the session objects they will
// always notify us before destruction so we can remove them from this list.
//
// The number of elements in |sessions_| acts as our scan reference count.
// When |sessions_| becomes empty scanning is stopped. Similarly, scanning is
// started on the insertion of the first element.
std::list<LowEnergyDiscoverySession*> sessions_;
// Identifiers for the cached scan results for the current scan period during
// discovery. The minimum (and default) scan period is 10.24 seconds
// when performing LE discovery. This can cause a long wait for a discovery
// session that joined in the middle of a scan period and duplicate filtering
// is enabled. We maintain this cache to immediately notify new sessions of
// the currently cached results for this period.
std::unordered_set<PeerId> cached_scan_results_;
// The value (in ms) that we use for the duration of each scan period.
zx::duration scan_period_ = kLEGeneralDiscoveryScanMin;
// Count of the number of outstanding PauseTokens. When |paused_count_| is 0, discovery is
// unpaused.
IntInspectable<int> paused_count_;
// The scanner that performs the HCI procedures. |scanner_| must out-live this
// discovery manager.
hci::LowEnergyScanner* scanner_; // weak
fit::thread_checker thread_checker_;
// Keep this as the last member to make sure that all weak pointers are
// invalidated before other members get destroyed.
fxl::WeakPtrFactory<LowEnergyDiscoveryManager> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyDiscoveryManager);
};
} // namespace gap
} // namespace bt
#endif // SRC_CONNECTIVITY_BLUETOOTH_CORE_BT_HOST_GAP_LOW_ENERGY_DISCOVERY_MANAGER_H_