blob: e1c9adbf9096a7c7c855faaf7a5afb1c7fdac8cf [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.
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gap/low_energy_discovery_manager.h"
#include <lib/fit/function.h>
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/assert.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gap/peer.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gap/peer_cache.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/transport/transport.h"
namespace bt::gap {
constexpr uint16_t kLEActiveScanInterval = 80; // 50ms
constexpr uint16_t kLEActiveScanWindow = 24; // 15ms
constexpr uint16_t kLEPassiveScanInterval = kLEScanSlowInterval1;
constexpr uint16_t kLEPassiveScanWindow = kLEScanSlowWindow1;
const char* kInspectPausedCountPropertyName = "paused";
const char* kInspectStatePropertyName = "state";
const char* kInspectFailedCountPropertyName = "failed_count";
const char* kInspectScanIntervalPropertyName = "scan_interval_ms";
const char* kInspectScanWindowPropertyName = "scan_window_ms";
LowEnergyDiscoverySession::LowEnergyDiscoverySession(
bool active, LowEnergyDiscoveryManager::WeakPtr manager)
: alive_(true), active_(active), manager_(std::move(manager)) {
BT_ASSERT(manager_.is_alive());
}
LowEnergyDiscoverySession::~LowEnergyDiscoverySession() {
if (alive_) {
Stop();
}
}
void LowEnergyDiscoverySession::SetResultCallback(PeerFoundCallback callback) {
peer_found_callback_ = std::move(callback);
if (!manager_.is_alive())
return;
for (PeerId cached_peer_id : manager_->cached_scan_results()) {
auto peer = manager_->peer_cache()->FindById(cached_peer_id);
// Ignore peers that have since been removed from the peer cache.
if (!peer) {
bt_log(TRACE,
"gap",
"Ignoring cached scan result for peer %s missing from peer cache",
bt_str(cached_peer_id));
continue;
}
NotifyDiscoveryResult(*peer);
}
}
void LowEnergyDiscoverySession::Stop() {
BT_DEBUG_ASSERT(alive_);
if (manager_.is_alive()) {
manager_->RemoveSession(this);
}
alive_ = false;
}
void LowEnergyDiscoverySession::NotifyDiscoveryResult(const Peer& peer) const {
BT_ASSERT(peer.le());
if (!alive_ || !peer_found_callback_) {
return;
}
if (filter_.MatchLowEnergyResult(peer.le()->parsed_advertising_data(),
peer.connectable(),
peer.rssi())) {
peer_found_callback_(peer);
}
}
void LowEnergyDiscoverySession::NotifyError() {
alive_ = false;
if (error_callback_) {
error_callback_();
}
}
LowEnergyDiscoveryManager::LowEnergyDiscoveryManager(
hci::LowEnergyScanner* scanner,
PeerCache* peer_cache,
pw::async::Dispatcher& dispatcher)
: WeakSelf(this),
dispatcher_(dispatcher),
state_(State::kIdle, StateToString),
peer_cache_(peer_cache),
paused_count_(0),
scanner_(scanner) {
BT_DEBUG_ASSERT(peer_cache_);
BT_DEBUG_ASSERT(scanner_);
scanner_->set_delegate(this);
}
LowEnergyDiscoveryManager::~LowEnergyDiscoveryManager() {
scanner_->set_delegate(nullptr);
DeactivateAndNotifySessions();
}
void LowEnergyDiscoveryManager::StartDiscovery(bool active,
SessionCallback callback) {
BT_ASSERT(callback);
bt_log(INFO, "gap-le", "start %s discovery", active ? "active" : "passive");
// If a request to start or stop is currently pending then this one will
// become pending until the HCI request completes. This does NOT include the
// state in which we are stopping and restarting scan in between scan
// periods, in which case session_ will not be empty.
//
// If the scan needs to be upgraded to an active scan, it will be handled in
// OnScanStatus() when the HCI request completes.
if (!pending_.empty() ||
(scanner_->state() == hci::LowEnergyScanner::State::kStopping &&
sessions_.empty())) {
BT_ASSERT(!scanner_->IsScanning());
pending_.push_back(
DiscoveryRequest{.active = active, .callback = std::move(callback)});
return;
}
// If a peer scan is already in progress, then the request succeeds (this
// includes the state in which we are stopping and restarting scan in between
// scan periods).
if (!sessions_.empty()) {
if (active) {
// If this is the first active session, stop scanning and wait for
// OnScanStatus() to initiate active scan.
if (!std::any_of(sessions_.begin(), sessions_.end(), [](auto s) {
return s->active_;
})) {
StopScan();
}
}
auto session = AddSession(active);
// Post the callback instead of calling it synchronously to avoid bugs
// caused by client code not expecting this.
(void)heap_dispatcher_.Post(
[callback = std::move(callback), session = std::move(session)](
pw::async::Context /*ctx*/, pw::Status status) mutable {
if (status.ok()) {
callback(std::move(session));
}
});
return;
}
pending_.push_back({.active = active, .callback = std::move(callback)});
if (paused()) {
return;
}
// If the scanner is not idle, it is starting/stopping, and the appropriate
// scanning will be initiated in OnScanStatus().
if (scanner_->IsIdle()) {
StartScan(active);
}
}
LowEnergyDiscoveryManager::PauseToken
LowEnergyDiscoveryManager::PauseDiscovery() {
if (!paused()) {
bt_log(TRACE, "gap-le", "Pausing discovery");
StopScan();
}
paused_count_.Set(*paused_count_ + 1);
return PauseToken([this, self = GetWeakPtr()]() {
if (!self.is_alive()) {
return;
}
BT_ASSERT(paused());
paused_count_.Set(*paused_count_ - 1);
if (*paused_count_ == 0) {
ResumeDiscovery();
}
});
}
bool LowEnergyDiscoveryManager::discovering() const {
return std::any_of(
sessions_.begin(), sessions_.end(), [](auto& s) { return s->active(); });
}
void LowEnergyDiscoveryManager::AttachInspect(inspect::Node& parent,
std::string name) {
inspect_.node = parent.CreateChild(name);
paused_count_.AttachInspect(inspect_.node, kInspectPausedCountPropertyName);
state_.AttachInspect(inspect_.node, kInspectStatePropertyName);
inspect_.failed_count =
inspect_.node.CreateUint(kInspectFailedCountPropertyName, 0);
inspect_.scan_interval_ms =
inspect_.node.CreateDouble(kInspectScanIntervalPropertyName, 0);
inspect_.scan_window_ms =
inspect_.node.CreateDouble(kInspectScanWindowPropertyName, 0);
}
std::string LowEnergyDiscoveryManager::StateToString(State state) {
switch (state) {
case State::kIdle:
return "Idle";
case State::kStarting:
return "Starting";
case State::kActive:
return "Active";
case State::kPassive:
return "Passive";
case State::kStopping:
return "Stopping";
}
}
std::unique_ptr<LowEnergyDiscoverySession>
LowEnergyDiscoveryManager::AddSession(bool active) {
// Cannot use make_unique here since LowEnergyDiscoverySession has a private
// constructor.
std::unique_ptr<LowEnergyDiscoverySession> session(
new LowEnergyDiscoverySession(active, GetWeakPtr()));
sessions_.push_back(session.get());
return session;
}
void LowEnergyDiscoveryManager::RemoveSession(
LowEnergyDiscoverySession* session) {
BT_ASSERT(session);
// Only alive sessions are allowed to call this method. If there is at least
// one alive session object out there, then we MUST be scanning.
BT_ASSERT(session->alive());
auto iter = std::find(sessions_.begin(), sessions_.end(), session);
BT_ASSERT(iter != sessions_.end());
bool active = session->active();
sessions_.erase(iter);
bool last_active = active && std::none_of(sessions_.begin(),
sessions_.end(),
[](auto& s) { return s->active_; });
// Stop scanning if the session count has dropped to zero or the scan type
// needs to be downgraded to passive.
if (sessions_.empty() || last_active) {
bt_log(TRACE,
"gap-le",
"Last %sdiscovery session removed, stopping scan (sessions: %zu)",
last_active ? "active " : "",
sessions_.size());
StopScan();
return;
}
}
void LowEnergyDiscoveryManager::OnPeerFound(
const hci::LowEnergyScanResult& result) {
bt_log(DEBUG,
"gap-le",
"peer found (address: %s, connectable: %d)",
bt_str(result.address()),
result.connectable());
auto peer = peer_cache_->FindByAddress(result.address());
if (peer && peer->connectable() && peer->le() && connectable_cb_) {
bt_log(TRACE,
"gap-le",
"found connectable peer (id: %s)",
bt_str(peer->identifier()));
connectable_cb_(peer);
}
// Don't notify sessions of unknown LE peers during passive scan.
if (scanner_->IsPassiveScanning() && (!peer || !peer->le())) {
return;
}
// Create a new entry if we found the device during general discovery.
if (!peer) {
peer = peer_cache_->NewPeer(result.address(), result.connectable());
BT_ASSERT(peer);
} else if (!peer->connectable() && result.connectable()) {
bt_log(DEBUG,
"gap-le",
"received connectable advertisement from previously non-connectable "
"peer (address: %s, "
"peer: %s)",
bt_str(result.address()),
bt_str(peer->identifier()));
peer->set_connectable(true);
}
peer->MutLe().SetAdvertisingData(
result.rssi(), result.data(), dispatcher_.now());
cached_scan_results_.insert(peer->identifier());
for (auto iter = sessions_.begin(); iter != sessions_.end();) {
// The session may be erased by the result handler, so we need to get the
// next iterator before iter is invalidated.
auto next = std::next(iter);
auto session = *iter;
session->NotifyDiscoveryResult(*peer);
iter = next;
}
}
void LowEnergyDiscoveryManager::OnDirectedAdvertisement(
const hci::LowEnergyScanResult& result) {
bt_log(TRACE,
"gap-le",
"Received directed advertisement (address: %s, %s)",
result.address().ToString().c_str(),
(result.resolved() ? "resolved" : "not resolved"));
auto peer = peer_cache_->FindByAddress(result.address());
if (!peer) {
bt_log(DEBUG,
"gap-le",
"ignoring connection request from unknown peripheral: %s",
result.address().ToString().c_str());
return;
}
if (!peer->le()) {
bt_log(DEBUG,
"gap-le",
"rejecting connection request from non-LE peripheral: %s",
result.address().ToString().c_str());
return;
}
if (peer->connectable() && connectable_cb_) {
connectable_cb_(peer);
}
// Only notify passive sessions.
for (auto iter = sessions_.begin(); iter != sessions_.end();) {
// The session may be erased by the result handler, so we need to get the
// next iterator before iter is invalidated.
auto next = std::next(iter);
auto session = *iter;
if (!session->active()) {
session->NotifyDiscoveryResult(*peer);
}
iter = next;
}
}
void LowEnergyDiscoveryManager::OnScanStatus(
hci::LowEnergyScanner::ScanStatus status) {
switch (status) {
case hci::LowEnergyScanner::ScanStatus::kFailed:
OnScanFailed();
return;
case hci::LowEnergyScanner::ScanStatus::kPassive:
OnPassiveScanStarted();
return;
case hci::LowEnergyScanner::ScanStatus::kActive:
OnActiveScanStarted();
return;
case hci::LowEnergyScanner::ScanStatus::kStopped:
OnScanStopped();
return;
case hci::LowEnergyScanner::ScanStatus::kComplete:
OnScanComplete();
return;
}
}
void LowEnergyDiscoveryManager::OnScanFailed() {
bt_log(ERROR, "gap-le", "failed to initiate scan!");
inspect_.failed_count.Add(1);
DeactivateAndNotifySessions();
// Report failure on all currently pending requests. If any of the
// callbacks issue a retry the new requests will get re-queued and
// notified of failure in the same loop here.
while (!pending_.empty()) {
auto request = std::move(pending_.back());
pending_.pop_back();
request.callback(nullptr);
}
state_.Set(State::kIdle);
}
void LowEnergyDiscoveryManager::OnPassiveScanStarted() {
bt_log(TRACE, "gap-le", "passive scan started");
state_.Set(State::kPassive);
// Stop the passive scan if an active scan was requested while the scan was
// starting. The active scan will start in OnScanStopped() once the passive
// scan stops.
if (std::any_of(sessions_.begin(),
sessions_.end(),
[](auto& s) { return s->active_; }) ||
std::any_of(
pending_.begin(), pending_.end(), [](auto& p) { return p.active; })) {
bt_log(TRACE,
"gap-le",
"active scan requested while passive scan was starting");
StopScan();
return;
}
NotifyPending();
}
void LowEnergyDiscoveryManager::OnActiveScanStarted() {
bt_log(TRACE, "gap-le", "active scan started");
state_.Set(State::kActive);
NotifyPending();
}
void LowEnergyDiscoveryManager::OnScanStopped() {
bt_log(DEBUG,
"gap-le",
"stopped scanning (paused: %d, pending: %zu, sessions: %zu)",
paused(),
pending_.size(),
sessions_.size());
state_.Set(State::kIdle);
cached_scan_results_.clear();
if (paused()) {
return;
}
if (!sessions_.empty()) {
bt_log(DEBUG, "gap-le", "initiating scanning");
bool active = std::any_of(
sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; });
StartScan(active);
return;
}
// Some clients might have requested to start scanning while we were
// waiting for it to stop. Restart scanning if that is the case.
if (!pending_.empty()) {
bt_log(DEBUG, "gap-le", "initiating scanning");
bool active = std::any_of(
pending_.begin(), pending_.end(), [](auto& p) { return p.active; });
StartScan(active);
return;
}
}
void LowEnergyDiscoveryManager::OnScanComplete() {
bt_log(TRACE, "gap-le", "end of scan period");
state_.Set(State::kIdle);
cached_scan_results_.clear();
if (paused()) {
return;
}
// If |sessions_| is empty this is because sessions were stopped while the
// scanner was shutting down after the end of the scan period. Restart the
// scan as long as clients are waiting for it.
ResumeDiscovery();
}
void LowEnergyDiscoveryManager::NotifyPending() {
// Create and register all sessions before notifying the clients. We do
// this so that the reference count is incremented for all new sessions
// before the callbacks execute, to prevent a potential case in which a
// callback stops its session immediately which could cause the reference
// count to drop the zero before all clients receive their session object.
if (!pending_.empty()) {
size_t count = pending_.size();
std::vector<std::unique_ptr<LowEnergyDiscoverySession>> new_sessions(count);
std::generate(new_sessions.begin(),
new_sessions.end(),
[this, i = size_t{0}]() mutable {
return AddSession(pending_[i++].active);
});
for (size_t i = count - 1; i < count; i--) {
auto cb = std::move(pending_.back().callback);
pending_.pop_back();
cb(std::move(new_sessions[i]));
}
}
BT_ASSERT(pending_.empty());
}
void LowEnergyDiscoveryManager::StartScan(bool active) {
auto cb = [self = GetWeakPtr()](auto status) {
if (self.is_alive())
self->OnScanStatus(status);
};
// TODO(armansito): A client that is interested in scanning nearby beacons and
// calculating proximity based on RSSI changes may want to disable duplicate
// filtering. We generally shouldn't allow this unless a client has the
// capability for it. Processing all HCI events containing advertising reports
// will both generate a lot of bus traffic and performing duplicate filtering
// on the host will take away CPU cycles from other things. It's a valid use
// case but needs proper management. For now we always make the controller
// filter duplicate reports.
hci::LowEnergyScanner::ScanOptions options{
.active = active,
.filter_duplicates = true,
.filter_policy =
pw::bluetooth::emboss::LEScanFilterPolicy::BASIC_UNFILTERED,
.period = scan_period_,
.scan_response_timeout = kLEScanResponseTimeout,
};
// See Vol 3, Part C, 9.3.11 "Connection Establishment Timing Parameters".
if (active) {
options.interval = kLEActiveScanInterval;
options.window = kLEActiveScanWindow;
} else {
options.interval = kLEPassiveScanInterval;
options.window = kLEPassiveScanWindow;
// TODO(armansito): Use the controller filter accept policy to filter
// advertisements.
}
// Since we use duplicate filtering, we stop and start the scan periodically
// to re-process advertisements. We use the minimum required scan period for
// general discovery (by default; |scan_period_| can be modified, e.g. by unit
// tests).
state_.Set(State::kStarting);
scanner_->StartScan(options, std::move(cb));
inspect_.scan_interval_ms.Set(HciScanIntervalToMs(options.interval));
inspect_.scan_window_ms.Set(HciScanWindowToMs(options.window));
}
void LowEnergyDiscoveryManager::StopScan() {
state_.Set(State::kStopping);
scanner_->StopScan();
}
void LowEnergyDiscoveryManager::ResumeDiscovery() {
BT_ASSERT(!paused());
if (!scanner_->IsIdle()) {
bt_log(TRACE, "gap-le", "attempt to resume discovery when it is not idle");
return;
}
if (!sessions_.empty()) {
bt_log(TRACE, "gap-le", "resuming scan");
bool active = std::any_of(
sessions_.begin(), sessions_.end(), [](auto& s) { return s->active_; });
StartScan(active);
return;
}
if (!pending_.empty()) {
bt_log(TRACE, "gap-le", "starting scan");
bool active = std::any_of(
pending_.begin(), pending_.end(), [](auto& s) { return s.active; });
StartScan(active);
return;
}
}
void LowEnergyDiscoveryManager::DeactivateAndNotifySessions() {
// If there are any active sessions we invalidate by notifying of an error.
// We move the initial set and notify those, if any error callbacks create
// additional sessions they will be added to pending_
auto sessions = std::move(sessions_);
for (const auto& session : sessions) {
if (session->alive()) {
session->NotifyError();
}
}
// Due to the move, sessions_ should be empty before the loop and any
// callbacks will add sessions to pending_ so it should be empty
// afterwards as well.
BT_ASSERT(sessions_.empty());
}
} // namespace bt::gap