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 #include "pw_bluetooth_sapphire/internal/host/gap/low_energy_connector.h"
16
17 #include "pw_bluetooth_sapphire/internal/host/gap/peer_cache.h"
18
19 namespace bt::gap::internal {
20
21 namespace {
22
23 // During the initial connection to a peripheral we use the initial high
24 // duty-cycle parameters to ensure that initiating procedures (bonding,
25 // encryption setup, service discovery) are completed quickly. Once these
26 // procedures are complete, we will change the connection interval to the
27 // peripheral's preferred connection parameters (see v5.0, Vol 3, Part C,
28 // Section 9.3.12).
29 static const hci_spec::LEPreferredConnectionParameters
30 kInitialConnectionParameters(kLEInitialConnIntervalMin,
31 kLEInitialConnIntervalMax,
32 /*max_latency=*/0,
33 hci_spec::defaults::kLESupervisionTimeout);
34
35 constexpr int kMaxConnectionAttempts = 3;
36 constexpr int kRetryExponentialBackoffBase = 2;
37
38 constexpr const char* kInspectPeerIdPropertyName = "peer_id";
39 constexpr const char* kInspectConnectionAttemptPropertyName =
40 "connection_attempt";
41 constexpr const char* kInspectStatePropertyName = "state";
42 constexpr const char* kInspectIsOutboundPropertyName = "is_outbound";
43
44 } // namespace
45
LowEnergyConnector(PeerId peer_id,LowEnergyConnectionOptions options,hci::Transport::WeakPtr hci,PeerCache * peer_cache,WeakSelf<LowEnergyConnectionManager>::WeakPtr conn_mgr,l2cap::ChannelManager * l2cap,gatt::GATT::WeakPtr gatt,const AdapterState & adapter_state,pw::async::Dispatcher & dispatcher)46 LowEnergyConnector::LowEnergyConnector(
47 PeerId peer_id,
48 LowEnergyConnectionOptions options,
49 hci::Transport::WeakPtr hci,
50 PeerCache* peer_cache,
51 WeakSelf<LowEnergyConnectionManager>::WeakPtr conn_mgr,
52 l2cap::ChannelManager* l2cap,
53 gatt::GATT::WeakPtr gatt,
54 const AdapterState& adapter_state,
55 pw::async::Dispatcher& dispatcher)
56 : dispatcher_(dispatcher),
57 peer_id_(peer_id),
58 peer_cache_(peer_cache),
59 l2cap_(l2cap),
60 gatt_(std::move(gatt)),
61 adapter_state_(adapter_state),
62 options_(options),
63 hci_(std::move(hci)),
64 le_connection_manager_(std::move(conn_mgr)) {
65 PW_CHECK(peer_cache_);
66 PW_CHECK(l2cap_);
67 PW_CHECK(gatt_.is_alive());
68 PW_CHECK(hci_.is_alive());
69 PW_CHECK(le_connection_manager_.is_alive());
70
71 cmd_ = hci_->command_channel()->AsWeakPtr();
72 PW_CHECK(cmd_.is_alive());
73
74 auto peer = peer_cache_->FindById(peer_id_);
75 PW_CHECK(peer);
76 peer_address_ = peer->address();
77
78 request_create_connection_task_.set_function(
79 [this](pw::async::Context /*ctx*/, pw::Status status) {
80 if (status.ok()) {
81 RequestCreateConnection();
82 }
83 });
84 }
85
~LowEnergyConnector()86 LowEnergyConnector::~LowEnergyConnector() {
87 if (*state_ != State::kComplete && *state_ != State::kFailed) {
88 bt_log(
89 WARN,
90 "gap-le",
91 "destroying LowEnergyConnector before procedure completed (peer: %s)",
92 bt_str(peer_id_));
93 NotifyFailure(ToResult(HostError::kCanceled));
94 }
95
96 if (hci_connector_ && hci_connector_->request_pending()) {
97 // NOTE: LowEnergyConnector will be unable to wait for the connection to be
98 // canceled. The hci::LowEnergyConnector may still be waiting to cancel the
99 // connection when a later gap::internal::LowEnergyConnector is created.
100 hci_connector_->Cancel();
101 }
102 }
103
StartOutbound(pw::chrono::SystemClock::duration request_timeout,hci::LowEnergyConnector * connector,LowEnergyDiscoveryManager::WeakPtr discovery_manager,ResultCallback cb)104 void LowEnergyConnector::StartOutbound(
105 pw::chrono::SystemClock::duration request_timeout,
106 hci::LowEnergyConnector* connector,
107 LowEnergyDiscoveryManager::WeakPtr discovery_manager,
108 ResultCallback cb) {
109 PW_CHECK(*state_ == State::kDefault);
110 PW_CHECK(discovery_manager.is_alive());
111 PW_CHECK(connector);
112 PW_CHECK(request_timeout.count() != 0);
113 hci_connector_ = connector;
114 discovery_manager_ = std::move(discovery_manager);
115 hci_request_timeout_ = request_timeout;
116 result_cb_ = std::move(cb);
117 set_is_outbound(true);
118
119 if (options_.auto_connect) {
120 RequestCreateConnection();
121 } else {
122 StartScanningForPeer();
123 }
124 }
125
StartInbound(std::unique_ptr<hci::LowEnergyConnection> connection,ResultCallback cb)126 void LowEnergyConnector::StartInbound(
127 std::unique_ptr<hci::LowEnergyConnection> connection, ResultCallback cb) {
128 PW_CHECK(*state_ == State::kDefault);
129 PW_CHECK(connection);
130 // Connection address should resolve to same peer as the given peer ID.
131 Peer* conn_peer = peer_cache_->FindByAddress(connection->peer_address());
132 PW_CHECK(conn_peer);
133 PW_CHECK(peer_id_ == conn_peer->identifier(),
134 "peer_id_ (%s) != connection peer (%s)",
135 bt_str(peer_id_),
136 bt_str(conn_peer->identifier()));
137 result_cb_ = std::move(cb);
138 set_is_outbound(false);
139
140 if (!InitializeConnection(std::move(connection))) {
141 return;
142 }
143
144 StartInterrogation();
145 }
146
Cancel()147 void LowEnergyConnector::Cancel() {
148 bt_log(INFO,
149 "gap-le",
150 "canceling connector (peer: %s, state: %s)",
151 bt_str(peer_id_),
152 StateToString(*state_));
153
154 switch (*state_) {
155 case State::kDefault:
156 // There is nothing to do if cancel is called before the procedure has
157 // started. There is no result callback to call yet.
158 break;
159 case State::kStartingScanning:
160 discovery_session_.reset();
161 NotifyFailure(ToResult(HostError::kCanceled));
162 break;
163 case State::kScanning:
164 discovery_session_.reset();
165 scan_timeout_task_.reset();
166 NotifyFailure(ToResult(HostError::kCanceled));
167 break;
168 case State::kConnecting:
169 // The connector will call the result callback with a cancelled result.
170 hci_connector_->Cancel();
171 break;
172 case State::kInterrogating:
173 // The interrogator will call the result callback with a cancelled result.
174 interrogator_->Cancel();
175 break;
176 case State::kPauseBeforeConnectionRetry:
177 request_create_connection_task_.Cancel();
178 NotifyFailure(ToResult(HostError::kCanceled));
179 break;
180 case State::kAwaitingConnectionFailedToBeEstablishedDisconnect:
181 // Waiting for disconnect complete, nothing to do.
182 case State::kComplete:
183 case State::kFailed:
184 // Cancelling completed/failed connector is a no-op.
185 break;
186 }
187 }
188
AttachInspect(inspect::Node & parent,std::string name)189 void LowEnergyConnector::AttachInspect(inspect::Node& parent,
190 std::string name) {
191 inspect_node_ = parent.CreateChild(name);
192 inspect_properties_.peer_id = inspect_node_.CreateString(
193 kInspectPeerIdPropertyName, peer_id_.ToString());
194 connection_attempt_.AttachInspect(inspect_node_,
195 kInspectConnectionAttemptPropertyName);
196 state_.AttachInspect(inspect_node_, kInspectStatePropertyName);
197 if (is_outbound_.has_value()) {
198 inspect_properties_.is_outbound =
199 inspect_node_.CreateBool(kInspectIsOutboundPropertyName, *is_outbound_);
200 }
201 }
202
StateToString(State state)203 const char* LowEnergyConnector::StateToString(State state) {
204 switch (state) {
205 case State::kDefault:
206 return "Default";
207 case State::kStartingScanning:
208 return "StartingScanning";
209 case State::kScanning:
210 return "Scanning";
211 case State::kConnecting:
212 return "Connecting";
213 case State::kInterrogating:
214 return "Interrogating";
215 case State::kAwaitingConnectionFailedToBeEstablishedDisconnect:
216 return "AwaitingConnectionFailedToBeEstablishedDisconnect";
217 case State::kPauseBeforeConnectionRetry:
218 return "PauseBeforeConnectionRetry";
219 case State::kComplete:
220 return "Complete";
221 case State::kFailed:
222 return "Failed";
223 }
224 }
225
StartScanningForPeer()226 void LowEnergyConnector::StartScanningForPeer() {
227 if (!discovery_manager_.is_alive()) {
228 return;
229 }
230 auto self = weak_self_.GetWeakPtr();
231
232 state_.Set(State::kStartingScanning);
233
234 discovery_manager_->StartDiscovery(/*active=*/false, [self](auto session) {
235 if (self.is_alive()) {
236 self->OnScanStart(std::move(session));
237 }
238 });
239 }
240
OnScanStart(LowEnergyDiscoverySessionPtr session)241 void LowEnergyConnector::OnScanStart(LowEnergyDiscoverySessionPtr session) {
242 if (*state_ == State::kFailed) {
243 return;
244 }
245 PW_CHECK(*state_ == State::kStartingScanning);
246
247 // Failed to start scan, abort connection procedure.
248 if (!session) {
249 bt_log(INFO, "gap-le", "failed to start scan (peer: %s)", bt_str(peer_id_));
250 NotifyFailure(ToResult(HostError::kFailed));
251 return;
252 }
253
254 bt_log(INFO,
255 "gap-le",
256 "started scanning for pending connection (peer: %s)",
257 bt_str(peer_id_));
258 state_.Set(State::kScanning);
259
260 auto self = weak_self_.GetWeakPtr();
261 scan_timeout_task_.emplace(
262 dispatcher_, [this](pw::async::Context& /*ctx*/, pw::Status status) {
263 if (!status.ok()) {
264 return;
265 }
266 PW_CHECK(*state_ == State::kScanning);
267 bt_log(INFO,
268 "gap-le",
269 "scan for pending connection timed out (peer: %s)",
270 bt_str(peer_id_));
271 NotifyFailure(ToResult(HostError::kTimedOut));
272 });
273 // The scan timeout may include time during which scanning is paused.
274 scan_timeout_task_->PostAfter(kLEGeneralCepScanTimeout);
275
276 discovery_session_ = std::move(session);
277 discovery_session_->filter()->set_connectable(true);
278
279 // The error callback must be set before the result callback in case the
280 // result callback is called synchronously.
281 discovery_session_->set_error_callback([self] {
282 PW_CHECK(self->state_.value() == State::kScanning);
283 bt_log(INFO,
284 "gap-le",
285 "discovery error while scanning for peer (peer: %s)",
286 bt_str(self->peer_id_));
287 self->scan_timeout_task_.reset();
288 self->NotifyFailure(ToResult(HostError::kFailed));
289 });
290
291 discovery_session_->SetResultCallback([self](auto& peer) {
292 PW_CHECK(self->state_.value() == State::kScanning);
293
294 if (peer.identifier() != self->peer_id_) {
295 return;
296 }
297
298 bt_log(INFO,
299 "gap-le",
300 "discovered peer for pending connection (peer: %s)",
301 bt_str(self->peer_id_));
302
303 self->scan_timeout_task_.reset();
304 self->discovery_session_->Stop();
305
306 self->RequestCreateConnection();
307 });
308 }
309
RequestCreateConnection()310 void LowEnergyConnector::RequestCreateConnection() {
311 // Scanning may be skipped. When the peer disconnects during/after
312 // interrogation, a retry may be initiated by calling this method.
313 PW_CHECK(*state_ == State::kDefault || *state_ == State::kScanning ||
314 *state_ == State::kPauseBeforeConnectionRetry);
315
316 // Pause discovery until connection complete.
317 std::optional<LowEnergyDiscoveryManager::PauseToken> pause_token;
318 if (discovery_manager_.is_alive()) {
319 pause_token = discovery_manager_->PauseDiscovery();
320 }
321
322 auto self = weak_self_.GetWeakPtr();
323 auto status_cb = [self, pause = std::move(pause_token)](hci::Result<> status,
324 auto link) {
325 if (self.is_alive()) {
326 self->OnConnectResult(status, std::move(link));
327 }
328 };
329
330 state_.Set(State::kConnecting);
331
332 // TODO(fxbug.dev/42149416): Use slow interval & window for auto connections
333 // during background scan.
334 PW_CHECK(hci_connector_->CreateConnection(
335 /*use_accept_list=*/false,
336 peer_address_,
337 kLEScanFastInterval,
338 kLEScanFastWindow,
339 kInitialConnectionParameters,
340 std::move(status_cb),
341 hci_request_timeout_));
342 }
343
OnConnectResult(hci::Result<> status,std::unique_ptr<hci::LowEnergyConnection> link)344 void LowEnergyConnector::OnConnectResult(
345 hci::Result<> status, std::unique_ptr<hci::LowEnergyConnection> link) {
346 if (status.is_error()) {
347 bt_log(INFO,
348 "gap-le",
349 "failed to connect to peer (id: %s, status: %s)",
350 bt_str(peer_id_),
351 bt_str(status));
352
353 NotifyFailure(status);
354 return;
355 }
356 PW_CHECK(link);
357
358 bt_log(INFO,
359 "gap-le",
360 "connection request successful (peer: %s)",
361 bt_str(peer_id_));
362
363 if (InitializeConnection(std::move(link))) {
364 StartInterrogation();
365 }
366 }
367
InitializeConnection(std::unique_ptr<hci::LowEnergyConnection> link)368 bool LowEnergyConnector::InitializeConnection(
369 std::unique_ptr<hci::LowEnergyConnection> link) {
370 PW_CHECK(link);
371
372 auto peer_disconnect_cb =
373 fit::bind_member<&LowEnergyConnector::OnPeerDisconnect>(this);
374 auto error_cb = [this]() { NotifyFailure(); };
375
376 Peer* peer = peer_cache_->FindById(peer_id_);
377 PW_CHECK(peer);
378 auto connection = LowEnergyConnection::Create(peer->GetWeakPtr(),
379 std::move(link),
380 options_,
381 peer_disconnect_cb,
382 error_cb,
383 le_connection_manager_,
384 l2cap_,
385 gatt_,
386 hci_,
387 dispatcher_);
388 if (!connection) {
389 bt_log(WARN,
390 "gap-le",
391 "connection initialization failed (peer: %s)",
392 bt_str(peer_id_));
393 NotifyFailure();
394 return false;
395 }
396
397 connection_ = std::move(connection);
398 return true;
399 }
400
StartInterrogation()401 void LowEnergyConnector::StartInterrogation() {
402 PW_CHECK((*is_outbound_ && *state_ == State::kConnecting) ||
403 (!*is_outbound_ && *state_ == State::kDefault));
404 PW_CHECK(connection_);
405
406 state_.Set(State::kInterrogating);
407 auto peer = peer_cache_->FindById(peer_id_);
408 PW_CHECK(peer);
409 bool sca_supported =
410 adapter_state_.SupportedCommands().le_request_peer_sca().Read();
411 interrogator_.emplace(
412 peer->GetWeakPtr(), connection_->handle(), cmd_, sca_supported);
413 interrogator_->Start(
414 fit::bind_member<&LowEnergyConnector::OnInterrogationComplete>(this));
415 }
416
OnInterrogationComplete(hci::Result<> status)417 void LowEnergyConnector::OnInterrogationComplete(hci::Result<> status) {
418 // If a disconnect event is received before interrogation completes, state_
419 // will be either kFailed or kPauseBeforeConnectionRetry depending on the
420 // status of the disconnect.
421 PW_CHECK(*state_ == State::kInterrogating || *state_ == State::kFailed ||
422 *state_ == State::kPauseBeforeConnectionRetry);
423 if (*state_ == State::kFailed ||
424 *state_ == State::kPauseBeforeConnectionRetry) {
425 return;
426 }
427
428 PW_CHECK(connection_);
429
430 // If the controller responds to an interrogation command with the 0x3e
431 // "kConnectionFailedToBeEstablished" error, it will send a Disconnection
432 // Complete event soon after. Wait for this event before initiating a retry.
433 if (status == ToResult(pw::bluetooth::emboss::StatusCode::
434 CONNECTION_FAILED_TO_BE_ESTABLISHED)) {
435 bt_log(INFO,
436 "gap-le",
437 "Received kConnectionFailedToBeEstablished during interrogation. "
438 "Waiting for Disconnect "
439 "Complete. (peer: %s)",
440 bt_str(peer_id_));
441 state_.Set(State::kAwaitingConnectionFailedToBeEstablishedDisconnect);
442 return;
443 }
444
445 if (status.is_error()) {
446 bt_log(INFO,
447 "gap-le",
448 "interrogation failed with %s (peer: %s)",
449 bt_str(status),
450 bt_str(peer_id_));
451 NotifyFailure();
452 return;
453 }
454
455 connection_->OnInterrogationComplete();
456 NotifySuccess();
457 }
458
OnPeerDisconnect(pw::bluetooth::emboss::StatusCode status_code)459 void LowEnergyConnector::OnPeerDisconnect(
460 pw::bluetooth::emboss::StatusCode status_code) {
461 // The peer can't disconnect while scanning or connecting, and we unregister
462 // from disconnects after kFailed & kComplete.
463 PW_CHECK(
464 *state_ == State::kInterrogating ||
465 *state_ == State::kAwaitingConnectionFailedToBeEstablishedDisconnect,
466 "Received peer disconnect during invalid state (state: %s, status: %s)",
467 StateToString(*state_),
468 bt_str(ToResult(status_code)));
469 if (*state_ == State::kInterrogating &&
470 status_code != pw::bluetooth::emboss::StatusCode::
471 CONNECTION_FAILED_TO_BE_ESTABLISHED) {
472 NotifyFailure(ToResult(status_code));
473 return;
474 }
475
476 // state_ is kAwaitingConnectionFailedToBeEstablished or kInterrogating with a
477 // 0x3e error, so retry connection
478 if (!MaybeRetryConnection()) {
479 NotifyFailure(ToResult(status_code));
480 }
481 }
482
MaybeRetryConnection()483 bool LowEnergyConnector::MaybeRetryConnection() {
484 // Only retry outbound connections.
485 if (*is_outbound_ && *connection_attempt_ < kMaxConnectionAttempts - 1) {
486 connection_.reset();
487 state_.Set(State::kPauseBeforeConnectionRetry);
488
489 // Exponential backoff (2s, 4s, 8s, ...)
490 std::chrono::seconds retry_delay(kRetryExponentialBackoffBase
491 << *connection_attempt_);
492
493 connection_attempt_.Set(*connection_attempt_ + 1);
494 bt_log(INFO,
495 "gap-le",
496 "Retrying connection in %llds (peer: %s, attempt: %d)",
497 retry_delay.count(),
498 bt_str(peer_id_),
499 *connection_attempt_);
500 request_create_connection_task_.PostAfter(retry_delay);
501 return true;
502 }
503 return false;
504 }
505
NotifySuccess()506 void LowEnergyConnector::NotifySuccess() {
507 PW_CHECK(*state_ == State::kInterrogating);
508 PW_CHECK(connection_);
509 PW_CHECK(result_cb_);
510
511 state_.Set(State::kComplete);
512
513 // LowEnergyConnectionManager should immediately set handlers to replace these
514 // ones.
515 connection_->set_peer_disconnect_callback([peer_id = peer_id_](auto) {
516 BT_PANIC("Peer disconnected without handler set (peer: %s)",
517 bt_str(peer_id));
518 });
519
520 connection_->set_error_callback([peer_id = peer_id_]() {
521 BT_PANIC("connection error without handler set (peer: %s)",
522 bt_str(peer_id));
523 });
524
525 result_cb_(fit::ok(std::move(connection_)));
526 }
527
NotifyFailure(hci::Result<> status)528 void LowEnergyConnector::NotifyFailure(hci::Result<> status) {
529 state_.Set(State::kFailed);
530 // The result callback must only be called once, so extraneous failures should
531 // be ignored.
532 if (result_cb_) {
533 result_cb_(fit::error(status.take_error()));
534 }
535 }
536
set_is_outbound(bool is_outbound)537 void LowEnergyConnector::set_is_outbound(bool is_outbound) {
538 is_outbound_ = is_outbound;
539 inspect_properties_.is_outbound =
540 inspect_node_.CreateBool(kInspectIsOutboundPropertyName, is_outbound);
541 }
542
543 } // namespace bt::gap::internal
544