| // 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 |