1 // Copyright 2024 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/central.h"
16
17 #include "pw_bluetooth_sapphire/internal/connection_options.h"
18 #include "pw_bluetooth_sapphire/internal/uuid.h"
19
20 namespace pw::bluetooth_sapphire {
21 namespace {
22
23 pw::sync::Mutex g_peripheral_lock;
24
DiscoveryFilterFrom(const Central::ScanFilter & in)25 bt::hci::DiscoveryFilter DiscoveryFilterFrom(const Central::ScanFilter& in) {
26 bt::hci::DiscoveryFilter out;
27 if (in.service_uuid.has_value()) {
28 bt::UUID uuid = internal::UuidFrom(in.service_uuid.value());
29 out.set_service_uuids(std::vector<bt::UUID>{std::move(uuid)});
30 }
31 if (in.service_data_uuid.has_value()) {
32 bt::UUID uuid = internal::UuidFrom(in.service_data_uuid.value());
33 out.set_service_data_uuids(std::vector<bt::UUID>{std::move(uuid)});
34 }
35 if (in.manufacturer_id.has_value()) {
36 out.set_manufacturer_code(in.manufacturer_id.value());
37 }
38 if (in.connectable.has_value()) {
39 out.set_connectable(in.connectable.value());
40 }
41 if (in.name.has_value()) {
42 out.set_name_substring(std::string(in.name.value()));
43 }
44 if (in.max_path_loss.has_value()) {
45 out.set_pathloss(in.max_path_loss.value());
46 }
47 if (in.solicitation_uuid.has_value()) {
48 bt::UUID uuid = internal::UuidFrom(in.solicitation_uuid.value());
49 out.set_solicitation_uuids(std::vector<bt::UUID>{std::move(uuid)});
50 }
51 return out;
52 }
53
ScanResultFrom(const bt::gap::Peer & peer,pw::multibuf::MultiBufAllocator & allocator)54 std::optional<Central::ScanResult> ScanResultFrom(
55 const bt::gap::Peer& peer, pw::multibuf::MultiBufAllocator& allocator) {
56 Central::ScanResult out;
57 out.peer_id = peer.identifier().value();
58 // TODO: https://pwbug.dev/377301546 - Report the "connectable" value of this
59 // advertisement, not the Peer's dual-mode connectability.
60 out.connectable = peer.connectable();
61 out.rssi = peer.rssi();
62
63 if (!peer.le()->parsed_advertising_data_timestamp().has_value()) {
64 bt_log(DEBUG, "api", "failed to get advertising data time");
65 return std::nullopt;
66 }
67 out.last_updated = peer.le()->parsed_advertising_data_timestamp().value();
68
69 bt::BufferView data_view = peer.le()->advertising_data();
70 std::optional<pw::multibuf::MultiBuf> data =
71 allocator.Allocate(data_view.size());
72 if (!data) {
73 bt_log(DEBUG, "api", "failed to allocate buffer for advertising data");
74 return std::nullopt;
75 }
76 StatusWithSize copy_status = data->CopyFrom(data_view.subspan());
77 if (!copy_status.ok()) {
78 bt_log(DEBUG,
79 "api",
80 "failed to copy scan result data: %s",
81 copy_status.status().str());
82 return std::nullopt;
83 }
84 out.data = std::move(data.value());
85
86 if (peer.name().has_value()) {
87 out.name.emplace();
88 Status append_status =
89 pw::string::Append(out.name.value(), peer.name().value());
90 // RESOURCE_EXHAUSTED means that the name was truncated, which is OK.
91 if (!append_status.ok() && !append_status.IsResourceExhausted()) {
92 bt_log(DEBUG,
93 "api",
94 "failed to set scan result name: %s",
95 append_status.str());
96 return std::nullopt;
97 }
98 }
99
100 return out;
101 }
102
103 } // namespace
104
Central(bt::gap::Adapter::WeakPtr adapter,pw::async::Dispatcher & dispatcher,pw::multibuf::MultiBufAllocator & allocator)105 Central::Central(bt::gap::Adapter::WeakPtr adapter,
106 pw::async::Dispatcher& dispatcher,
107 pw::multibuf::MultiBufAllocator& allocator)
108 : adapter_(std::move(adapter)),
109 dispatcher_(dispatcher),
110 heap_dispatcher_(dispatcher),
111 allocator_(allocator),
112 weak_factory_(this),
113 self_(weak_factory_.GetWeakPtr()) {}
114
~Central()115 Central::~Central() {
116 std::lock_guard guard(lock());
117 scans_.clear();
118 }
119
Connect(pw::bluetooth::PeerId peer_id,bluetooth::low_energy::Connection2::ConnectionOptions options)120 async2::OnceReceiver<Central::ConnectResult> Central::Connect(
121 pw::bluetooth::PeerId peer_id,
122 bluetooth::low_energy::Connection2::ConnectionOptions options) {
123 bt::PeerId internal_peer_id(peer_id);
124 bt::gap::LowEnergyConnectionOptions connection_options =
125 internal::ConnectionOptionsFrom(options);
126
127 auto [result_sender, result_receiver] =
128 async2::MakeOnceSenderAndReceiver<ConnectResult>();
129
130 bt::gap::Adapter::LowEnergy::ConnectionResultCallback result_cb =
131 [self = self_,
132 peer = internal_peer_id,
133 sender = std::move(result_sender)](
134 bt::gap::Adapter::LowEnergy::ConnectionResult result) mutable {
135 if (!self.is_alive()) {
136 return;
137 }
138 self->OnConnectionResult(peer, std::move(result), std::move(sender));
139 };
140
141 async::TaskFunction task_fn = [self = self_,
142 internal_peer_id,
143 connection_options,
144 cb = std::move(result_cb)](
145 async::Context&, Status status) mutable {
146 if (!status.ok() || !self.is_alive()) {
147 return;
148 }
149 self->adapter_->le()->Connect(
150 internal_peer_id, std::move(cb), connection_options);
151 };
152 Status post_status = heap_dispatcher_.Post(std::move(task_fn));
153 PW_CHECK_OK(post_status);
154
155 return std::move(result_receiver);
156 }
157
Scan(const ScanOptions & options)158 async2::OnceReceiver<Central::ScanStartResult> Central::Scan(
159 const ScanOptions& options) {
160 // TODO: https://pwbug.dev/377301546 - Support the different types of active
161 // scans.
162 bool active = (options.scan_type != ScanType::kPassive);
163
164 if (options.filters.empty()) {
165 return async2::OnceReceiver<ScanStartResult>(
166 pw::unexpected(StartScanError::kInvalidParameters));
167 }
168
169 auto [result_sender, result_receiver] =
170 async2::MakeOnceSenderAndReceiver<Central::ScanStartResult>();
171
172 auto callback =
173 [self = self_, sender = std::move(result_sender)](
174 std::unique_ptr<bt::gap::LowEnergyDiscoverySession> session) mutable {
175 // callback will always be run on the Bluetooth thread
176
177 if (!self.is_alive()) {
178 sender.emplace(pw::unexpected(StartScanError::kInternal));
179 return;
180 }
181
182 if (!session) {
183 bt_log(WARN, "api", "failed to start LE discovery session");
184 sender.emplace(pw::unexpected(StartScanError::kInternal));
185 return;
186 }
187
188 ScanHandleImpl* scan_handle_raw_ptr =
189 new ScanHandleImpl(session->scan_id(), &self.get());
190 ScanHandle::Ptr scan_handle_ptr(scan_handle_raw_ptr);
191 {
192 std::lock_guard guard(lock());
193 auto [iter, emplaced] = self->scans_.try_emplace(session->scan_id(),
194 std::move(session),
195 scan_handle_raw_ptr,
196 session->scan_id(),
197 &self.get());
198 PW_CHECK(emplaced);
199 }
200
201 sender.emplace(std::move(scan_handle_ptr));
202 };
203
204 // Convert options to filters now because options contains non-owning views
205 // that won't be valid in callbacks.
206 std::vector<bt::hci::DiscoveryFilter> discovery_filters;
207 discovery_filters.reserve(options.filters.size());
208 for (const ScanFilter& filter : options.filters) {
209 discovery_filters.emplace_back(DiscoveryFilterFrom(filter));
210 }
211
212 async::TaskFunction task_fn = [self = self_,
213 filters = std::move(discovery_filters),
214 active,
215 cb = std::move(callback)](
216 async::Context&, Status status) mutable {
217 if (status.ok()) {
218 // TODO: https://pwbug.dev/377301546 - Support configuring interval,
219 // window, and PHY.
220 self->adapter_->le()->StartDiscovery(active, filters, std::move(cb));
221 }
222 };
223 Status post_status = heap_dispatcher_.Post(std::move(task_fn));
224 PW_CHECK_OK(post_status);
225
226 return std::move(result_receiver);
227 }
228
lock()229 pw::sync::Mutex& Central::lock() { return g_peripheral_lock; }
230
~ScanHandleImpl()231 Central::ScanHandleImpl::~ScanHandleImpl() {
232 std::lock_guard guard(lock());
233 if (central_) {
234 central_->StopScanLocked(scan_id_);
235 }
236 }
237
QueueScanResultLocked(ScanResult && result)238 void Central::ScanHandleImpl::QueueScanResultLocked(ScanResult&& result) {
239 if (results_.size() == kMaxScanResultsQueueSize) {
240 results_.pop();
241 }
242 results_.push(std::move(result));
243 std::move(waker_).Wake();
244 }
245
246 async2::Poll<pw::Result<Central::ScanResult>>
PendResult(async2::Context & cx)247 Central::ScanHandleImpl::PendResult(async2::Context& cx) {
248 std::lock_guard guard(lock());
249 if (!results_.empty()) {
250 ScanResult result = std::move(results_.front());
251 results_.pop();
252 return async2::Ready(std::move(result));
253 }
254
255 if (!central_) {
256 return async2::Ready(pw::Status::Cancelled());
257 }
258
259 PW_ASYNC_STORE_WAKER(cx, waker_, "scan result");
260 return async2::Pending();
261 }
262
ScanState(std::unique_ptr<bt::gap::LowEnergyDiscoverySession> session,ScanHandleImpl * scan_handle,uint16_t scan_id,Central * central)263 Central::ScanState::ScanState(
264 std::unique_ptr<bt::gap::LowEnergyDiscoverySession> session,
265 ScanHandleImpl* scan_handle,
266 uint16_t scan_id,
267 Central* central)
268 : scan_id_(scan_id),
269 scan_handle_(scan_handle),
270 central_(central),
271 session_(std::move(session)) {
272 session_->SetResultCallback(
273 [this](const bt::gap::Peer& peer) { OnScanResult(peer); });
274 session_->set_error_callback([this]() { OnError(); });
275 }
276
~ScanState()277 Central::ScanState::~ScanState() {
278 // lock() is expected to be held
279 if (scan_handle_) {
280 scan_handle_->OnScanErrorLocked();
281 scan_handle_ = nullptr;
282 }
283 }
284
OnScanResult(const bt::gap::Peer & peer)285 void Central::ScanState::OnScanResult(const bt::gap::Peer& peer) {
286 // TODO: https://pwbug.dev/377301546 - Getting only a Peer as a scan result is
287 // awkward. Update LowEnergyDiscoverySession to give us the actual
288 // LowEnergyScanResult.
289 std::lock_guard guard(lock());
290 if (!scan_handle_) {
291 return;
292 }
293
294 std::optional<Central::ScanResult> scan_result =
295 ScanResultFrom(peer, central_->allocator_);
296 if (!scan_result.has_value()) {
297 return;
298 }
299
300 scan_handle_->QueueScanResultLocked(std::move(scan_result.value()));
301 scan_handle_->WakeLocked();
302 }
303
OnError()304 void Central::ScanState::OnError() {
305 std::lock_guard guard(lock());
306 if (scan_handle_) {
307 scan_handle_->OnScanErrorLocked();
308 scan_handle_ = nullptr;
309 }
310 central_->scans_.erase(scan_id_);
311 // This object has been destroyed.
312 }
313
StopScanLocked(uint16_t scan_id)314 void Central::StopScanLocked(uint16_t scan_id) {
315 auto iter = scans_.find(scan_id);
316 if (iter == scans_.end()) {
317 return;
318 }
319 iter->second.OnScanHandleDestroyedLocked();
320
321 pw::Status post_status = heap_dispatcher_.Post(
322 [self = self_, scan_id](pw::async::Context, pw::Status status) {
323 if (!status.ok() || !self.is_alive()) {
324 return;
325 }
326 std::lock_guard guard(lock());
327 self->scans_.erase(scan_id);
328 });
329 PW_CHECK(post_status.ok());
330 }
331
OnConnectionResult(bt::PeerId peer_id,bt::gap::Adapter::LowEnergy::ConnectionResult result,async2::OnceSender<ConnectResult> result_sender)332 void Central::OnConnectionResult(
333 bt::PeerId peer_id,
334 bt::gap::Adapter::LowEnergy::ConnectionResult result,
335 async2::OnceSender<ConnectResult> result_sender) {
336 if (result.is_error()) {
337 if (result.error_value() == bt::HostError::kNotFound) {
338 result_sender.emplace(pw::unexpected(ConnectError::kUnknownPeer));
339 } else {
340 result_sender.emplace(
341 pw::unexpected(ConnectError::kCouldNotBeEstablished));
342 }
343 return;
344 }
345
346 pw::bluetooth::low_energy::Connection2::Ptr connection_ptr(
347 new internal::Connection(
348 peer_id, std::move(result.value()), dispatcher_));
349 result_sender.emplace(std::move(connection_ptr));
350 }
351
352 } // namespace pw::bluetooth_sapphire
353