blob: 115a431fe084e5bb47a7a7fc95410cd2968c2eff [file] [log] [blame]
// Copyright 2021 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_connector.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gap/peer_cache.h"
namespace bt::gap::internal {
namespace {
// During the initial connection to a peripheral we use the initial high
// duty-cycle parameters to ensure that initiating procedures (bonding,
// encryption setup, service discovery) are completed quickly. Once these
// procedures are complete, we will change the connection interval to the
// peripheral's preferred connection parameters (see v5.0, Vol 3, Part C,
// Section 9.3.12).
static const hci_spec::LEPreferredConnectionParameters
kInitialConnectionParameters(kLEInitialConnIntervalMin,
kLEInitialConnIntervalMax,
/*max_latency=*/0,
hci_spec::defaults::kLESupervisionTimeout);
constexpr int kMaxConnectionAttempts = 3;
constexpr int kRetryExponentialBackoffBase = 2;
constexpr const char* kInspectPeerIdPropertyName = "peer_id";
constexpr const char* kInspectConnectionAttemptPropertyName =
"connection_attempt";
constexpr const char* kInspectStatePropertyName = "state";
constexpr const char* kInspectIsOutboundPropertyName = "is_outbound";
} // namespace
LowEnergyConnector::LowEnergyConnector(
PeerId peer_id,
LowEnergyConnectionOptions options,
hci::CommandChannel::WeakPtr cmd_channel,
PeerCache* peer_cache,
WeakSelf<LowEnergyConnectionManager>::WeakPtr conn_mgr,
l2cap::ChannelManager* l2cap,
gatt::GATT::WeakPtr gatt,
const AdapterState& adapter_state,
pw::async::Dispatcher& dispatcher)
: dispatcher_(dispatcher),
peer_id_(peer_id),
peer_cache_(peer_cache),
l2cap_(l2cap),
gatt_(std::move(gatt)),
adapter_state_(adapter_state),
options_(options),
cmd_(std::move(cmd_channel)),
le_connection_manager_(std::move(conn_mgr)) {
BT_ASSERT(cmd_.is_alive());
BT_ASSERT(peer_cache_);
BT_ASSERT(l2cap_);
BT_ASSERT(gatt_.is_alive());
BT_ASSERT(le_connection_manager_.is_alive());
auto peer = peer_cache_->FindById(peer_id_);
BT_ASSERT(peer);
peer_address_ = peer->address();
request_create_connection_task_.set_function(
[this](pw::async::Context /*ctx*/, pw::Status status) {
if (status.ok()) {
RequestCreateConnection();
}
});
}
LowEnergyConnector::~LowEnergyConnector() {
if (*state_ != State::kComplete && *state_ != State::kFailed) {
bt_log(
WARN,
"gap-le",
"destroying LowEnergyConnector before procedure completed (peer: %s)",
bt_str(peer_id_));
NotifyFailure(ToResult(HostError::kCanceled));
}
if (hci_connector_ && hci_connector_->request_pending()) {
// NOTE: LowEnergyConnector will be unable to wait for the connection to be
// canceled. The hci::LowEnergyConnector may still be waiting to cancel the
// connection when a later gap::internal::LowEnergyConnector is created.
hci_connector_->Cancel();
}
}
void LowEnergyConnector::StartOutbound(
pw::chrono::SystemClock::duration request_timeout,
hci::LowEnergyConnector* connector,
LowEnergyDiscoveryManager::WeakPtr discovery_manager,
ResultCallback cb) {
BT_ASSERT(*state_ == State::kDefault);
BT_ASSERT(discovery_manager.is_alive());
BT_ASSERT(connector);
BT_ASSERT(request_timeout.count() != 0);
hci_connector_ = connector;
discovery_manager_ = std::move(discovery_manager);
hci_request_timeout_ = request_timeout;
result_cb_ = std::move(cb);
set_is_outbound(true);
if (options_.auto_connect) {
RequestCreateConnection();
} else {
StartScanningForPeer();
}
}
void LowEnergyConnector::StartInbound(
std::unique_ptr<hci::LowEnergyConnection> connection, ResultCallback cb) {
BT_ASSERT(*state_ == State::kDefault);
BT_ASSERT(connection);
// Connection address should resolve to same peer as the given peer ID.
Peer* conn_peer = peer_cache_->FindByAddress(connection->peer_address());
BT_ASSERT(conn_peer);
BT_ASSERT_MSG(peer_id_ == conn_peer->identifier(),
"peer_id_ (%s) != connection peer (%s)",
bt_str(peer_id_),
bt_str(conn_peer->identifier()));
result_cb_ = std::move(cb);
set_is_outbound(false);
if (!InitializeConnection(std::move(connection))) {
return;
}
StartInterrogation();
}
void LowEnergyConnector::Cancel() {
bt_log(INFO,
"gap-le",
"canceling connector (peer: %s, state: %s)",
bt_str(peer_id_),
StateToString(*state_));
switch (*state_) {
case State::kDefault:
// There is nothing to do if cancel is called before the procedure has
// started. There is no result callback to call yet.
break;
case State::kStartingScanning:
discovery_session_.reset();
NotifyFailure(ToResult(HostError::kCanceled));
break;
case State::kScanning:
discovery_session_.reset();
scan_timeout_task_.reset();
NotifyFailure(ToResult(HostError::kCanceled));
break;
case State::kConnecting:
// The connector will call the result callback with a cancelled result.
hci_connector_->Cancel();
break;
case State::kInterrogating:
// The interrogator will call the result callback with a cancelled result.
interrogator_->Cancel();
break;
case State::kPauseBeforeConnectionRetry:
request_create_connection_task_.Cancel();
NotifyFailure(ToResult(HostError::kCanceled));
break;
case State::kAwaitingConnectionFailedToBeEstablishedDisconnect:
// Waiting for disconnect complete, nothing to do.
case State::kComplete:
case State::kFailed:
// Cancelling completed/failed connector is a no-op.
break;
}
}
void LowEnergyConnector::AttachInspect(inspect::Node& parent,
std::string name) {
inspect_node_ = parent.CreateChild(name);
inspect_properties_.peer_id = inspect_node_.CreateString(
kInspectPeerIdPropertyName, peer_id_.ToString());
connection_attempt_.AttachInspect(inspect_node_,
kInspectConnectionAttemptPropertyName);
state_.AttachInspect(inspect_node_, kInspectStatePropertyName);
if (is_outbound_.has_value()) {
inspect_properties_.is_outbound =
inspect_node_.CreateBool(kInspectIsOutboundPropertyName, *is_outbound_);
}
}
const char* LowEnergyConnector::StateToString(State state) {
switch (state) {
case State::kDefault:
return "Default";
case State::kStartingScanning:
return "StartingScanning";
case State::kScanning:
return "Scanning";
case State::kConnecting:
return "Connecting";
case State::kInterrogating:
return "Interrogating";
case State::kAwaitingConnectionFailedToBeEstablishedDisconnect:
return "AwaitingConnectionFailedToBeEstablishedDisconnect";
case State::kPauseBeforeConnectionRetry:
return "PauseBeforeConnectionRetry";
case State::kComplete:
return "Complete";
case State::kFailed:
return "Failed";
}
}
void LowEnergyConnector::StartScanningForPeer() {
if (!discovery_manager_.is_alive()) {
return;
}
auto self = weak_self_.GetWeakPtr();
state_.Set(State::kStartingScanning);
discovery_manager_->StartDiscovery(/*active=*/false, [self](auto session) {
if (self.is_alive()) {
self->OnScanStart(std::move(session));
}
});
}
void LowEnergyConnector::OnScanStart(LowEnergyDiscoverySessionPtr session) {
if (*state_ == State::kFailed) {
return;
}
BT_ASSERT(*state_ == State::kStartingScanning);
// Failed to start scan, abort connection procedure.
if (!session) {
bt_log(INFO, "gap-le", "failed to start scan (peer: %s)", bt_str(peer_id_));
NotifyFailure(ToResult(HostError::kFailed));
return;
}
bt_log(INFO,
"gap-le",
"started scanning for pending connection (peer: %s)",
bt_str(peer_id_));
state_.Set(State::kScanning);
auto self = weak_self_.GetWeakPtr();
scan_timeout_task_.emplace(
dispatcher_, [this](pw::async::Context& /*ctx*/, pw::Status status) {
if (!status.ok()) {
return;
}
BT_ASSERT(*state_ == State::kScanning);
bt_log(INFO,
"gap-le",
"scan for pending connection timed out (peer: %s)",
bt_str(peer_id_));
NotifyFailure(ToResult(HostError::kTimedOut));
});
// The scan timeout may include time during which scanning is paused.
scan_timeout_task_->PostAfter(kLEGeneralCepScanTimeout);
discovery_session_ = std::move(session);
discovery_session_->filter()->set_connectable(true);
// The error callback must be set before the result callback in case the
// result callback is called synchronously.
discovery_session_->set_error_callback([self] {
BT_ASSERT(self->state_.value() == State::kScanning);
bt_log(INFO,
"gap-le",
"discovery error while scanning for peer (peer: %s)",
bt_str(self->peer_id_));
self->scan_timeout_task_.reset();
self->NotifyFailure(ToResult(HostError::kFailed));
});
discovery_session_->SetResultCallback([self](auto& peer) {
BT_ASSERT(self->state_.value() == State::kScanning);
if (peer.identifier() != self->peer_id_) {
return;
}
bt_log(INFO,
"gap-le",
"discovered peer for pending connection (peer: %s)",
bt_str(self->peer_id_));
self->scan_timeout_task_.reset();
self->discovery_session_->Stop();
self->RequestCreateConnection();
});
}
void LowEnergyConnector::RequestCreateConnection() {
// Scanning may be skipped. When the peer disconnects during/after
// interrogation, a retry may be initiated by calling this method.
BT_ASSERT(*state_ == State::kDefault || *state_ == State::kScanning ||
*state_ == State::kPauseBeforeConnectionRetry);
// Pause discovery until connection complete.
std::optional<LowEnergyDiscoveryManager::PauseToken> pause_token;
if (discovery_manager_.is_alive()) {
pause_token = discovery_manager_->PauseDiscovery();
}
auto self = weak_self_.GetWeakPtr();
auto status_cb = [self, pause = std::move(pause_token)](hci::Result<> status,
auto link) {
if (self.is_alive()) {
self->OnConnectResult(status, std::move(link));
}
};
state_.Set(State::kConnecting);
// TODO(https://fxbug.dev/42149416): Use slow interval & window for auto
// connections during background scan.
BT_ASSERT(hci_connector_->CreateConnection(
/*use_accept_list=*/false,
peer_address_,
kLEScanFastInterval,
kLEScanFastWindow,
kInitialConnectionParameters,
std::move(status_cb),
hci_request_timeout_));
}
void LowEnergyConnector::OnConnectResult(
hci::Result<> status, std::unique_ptr<hci::LowEnergyConnection> link) {
if (status.is_error()) {
bt_log(INFO,
"gap-le",
"failed to connect to peer (id: %s, status: %s)",
bt_str(peer_id_),
bt_str(status));
NotifyFailure(status);
return;
}
BT_ASSERT(link);
bt_log(INFO,
"gap-le",
"connection request successful (peer: %s)",
bt_str(peer_id_));
if (InitializeConnection(std::move(link))) {
StartInterrogation();
}
}
bool LowEnergyConnector::InitializeConnection(
std::unique_ptr<hci::LowEnergyConnection> link) {
BT_ASSERT(link);
auto peer_disconnect_cb =
fit::bind_member<&LowEnergyConnector::OnPeerDisconnect>(this);
auto error_cb = [this]() { NotifyFailure(); };
Peer* peer = peer_cache_->FindById(peer_id_);
BT_ASSERT(peer);
auto connection = LowEnergyConnection::Create(peer->GetWeakPtr(),
std::move(link),
options_,
peer_disconnect_cb,
error_cb,
le_connection_manager_,
l2cap_,
gatt_,
cmd_,
dispatcher_);
if (!connection) {
bt_log(WARN,
"gap-le",
"connection initialization failed (peer: %s)",
bt_str(peer_id_));
NotifyFailure();
return false;
}
connection_ = std::move(connection);
return true;
}
void LowEnergyConnector::StartInterrogation() {
BT_ASSERT((*is_outbound_ && *state_ == State::kConnecting) ||
(!*is_outbound_ && *state_ == State::kDefault));
BT_ASSERT(connection_);
state_.Set(State::kInterrogating);
auto peer = peer_cache_->FindById(peer_id_);
BT_ASSERT(peer);
bool sca_supported = adapter_state_.IsCommandSupported(
/*octet=*/43, hci_spec::SupportedCommand::kLERequestPeerSCA);
interrogator_.emplace(
peer->GetWeakPtr(), connection_->handle(), cmd_, sca_supported);
interrogator_->Start(
fit::bind_member<&LowEnergyConnector::OnInterrogationComplete>(this));
}
void LowEnergyConnector::OnInterrogationComplete(hci::Result<> status) {
// If a disconnect event is received before interrogation completes, state_
// will be either kFailed or kPauseBeforeConnectionRetry depending on the
// status of the disconnect.
BT_ASSERT(*state_ == State::kInterrogating || *state_ == State::kFailed ||
*state_ == State::kPauseBeforeConnectionRetry);
if (*state_ == State::kFailed ||
*state_ == State::kPauseBeforeConnectionRetry) {
return;
}
BT_ASSERT(connection_);
// If the controller responds to an interrogation command with the 0x3e
// "kConnectionFailedToBeEstablished" error, it will send a Disconnection
// Complete event soon after. Wait for this event before initiating a retry.
if (status == ToResult(pw::bluetooth::emboss::StatusCode::
CONNECTION_FAILED_TO_BE_ESTABLISHED)) {
bt_log(INFO,
"gap-le",
"Received kConnectionFailedToBeEstablished during interrogation. "
"Waiting for Disconnect "
"Complete. (peer: %s)",
bt_str(peer_id_));
state_.Set(State::kAwaitingConnectionFailedToBeEstablishedDisconnect);
return;
}
if (status.is_error()) {
bt_log(INFO,
"gap-le",
"interrogation failed with %s (peer: %s)",
bt_str(status),
bt_str(peer_id_));
NotifyFailure();
return;
}
connection_->OnInterrogationComplete();
NotifySuccess();
}
void LowEnergyConnector::OnPeerDisconnect(
pw::bluetooth::emboss::StatusCode status_code) {
// The peer can't disconnect while scanning or connecting, and we unregister
// from disconnects after kFailed & kComplete.
BT_ASSERT_MSG(
*state_ == State::kInterrogating ||
*state_ == State::kAwaitingConnectionFailedToBeEstablishedDisconnect,
"Received peer disconnect during invalid state (state: %s, status: %s)",
StateToString(*state_),
bt_str(ToResult(status_code)));
if (*state_ == State::kInterrogating &&
status_code != pw::bluetooth::emboss::StatusCode::
CONNECTION_FAILED_TO_BE_ESTABLISHED) {
NotifyFailure(ToResult(status_code));
return;
}
// state_ is kAwaitingConnectionFailedToBeEstablished or kInterrogating with a
// 0x3e error, so retry connection
if (!MaybeRetryConnection()) {
NotifyFailure(ToResult(status_code));
}
}
bool LowEnergyConnector::MaybeRetryConnection() {
// Only retry outbound connections.
if (*is_outbound_ && *connection_attempt_ < kMaxConnectionAttempts - 1) {
connection_.reset();
state_.Set(State::kPauseBeforeConnectionRetry);
// Exponential backoff (2s, 4s, 8s, ...)
std::chrono::seconds retry_delay(kRetryExponentialBackoffBase
<< *connection_attempt_);
connection_attempt_.Set(*connection_attempt_ + 1);
bt_log(INFO,
"gap-le",
"Retrying connection in %llds (peer: %s, attempt: %d)",
retry_delay.count(),
bt_str(peer_id_),
*connection_attempt_);
request_create_connection_task_.PostAfter(retry_delay);
return true;
}
return false;
}
void LowEnergyConnector::NotifySuccess() {
BT_ASSERT(*state_ == State::kInterrogating);
BT_ASSERT(connection_);
BT_ASSERT(result_cb_);
state_.Set(State::kComplete);
// LowEnergyConnectionManager should immediately set handlers to replace these
// ones.
connection_->set_peer_disconnect_callback([peer_id = peer_id_](auto) {
BT_PANIC("Peer disconnected without handler set (peer: %s)",
bt_str(peer_id));
});
connection_->set_error_callback([peer_id = peer_id_]() {
BT_PANIC("connection error without handler set (peer: %s)",
bt_str(peer_id));
});
result_cb_(fit::ok(std::move(connection_)));
}
void LowEnergyConnector::NotifyFailure(hci::Result<> status) {
state_.Set(State::kFailed);
// The result callback must only be called once, so extraneous failures should
// be ignored.
if (result_cb_) {
result_cb_(fit::error(status.take_error()));
}
}
void LowEnergyConnector::set_is_outbound(bool is_outbound) {
is_outbound_ = is_outbound;
inspect_properties_.is_outbound =
inspect_node_.CreateBool(kInspectIsOutboundPropertyName, is_outbound);
}
} // namespace bt::gap::internal