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