| // Copyright 2024 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_bluetooth_sapphire/central.h" |
| |
| #include "pw_bluetooth_sapphire/internal/connection_options.h" |
| #include "pw_bluetooth_sapphire/internal/uuid.h" |
| |
| namespace pw::bluetooth_sapphire { |
| |
| bt::hci::DiscoveryFilter DiscoveryFilterFrom(const Central::ScanFilter& in) { |
| bt::hci::DiscoveryFilter out; |
| if (in.service_uuid.has_value()) { |
| bt::UUID uuid = internal::UuidFrom(in.service_uuid.value()); |
| out.set_service_uuids({std::move(uuid)}); |
| } |
| if (in.service_data_uuid.has_value()) { |
| bt::UUID uuid = internal::UuidFrom(in.service_data_uuid.value()); |
| out.set_service_data_uuids({std::move(uuid)}); |
| } |
| if (in.manufacturer_id.has_value()) { |
| out.set_manufacturer_code(in.manufacturer_id.value()); |
| } |
| if (in.connectable.has_value()) { |
| out.set_connectable(in.connectable.value()); |
| } |
| if (in.name.has_value()) { |
| out.set_name_substring(std::string(in.name.value())); |
| } |
| if (in.max_path_loss.has_value()) { |
| out.set_pathloss(in.max_path_loss.value()); |
| } |
| if (in.solicitation_uuid.has_value()) { |
| bt::UUID uuid = internal::UuidFrom(in.solicitation_uuid.value()); |
| out.set_solicitation_uuids({std::move(uuid)}); |
| } |
| return out; |
| } |
| |
| namespace { |
| |
| pw::sync::Mutex g_peripheral_lock; |
| |
| std::optional<Central::ScanResult> ScanResultFrom( |
| const bt::gap::Peer& peer, pw::multibuf::MultiBufAllocator& allocator) { |
| Central::ScanResult out; |
| out.peer_id = peer.identifier().value(); |
| // TODO: https://pwbug.dev/377301546 - Report the "connectable" value of this |
| // advertisement, not the Peer's dual-mode connectability. |
| out.connectable = peer.connectable(); |
| out.rssi = peer.rssi(); |
| |
| if (!peer.le()->parsed_advertising_data_timestamp().has_value()) { |
| bt_log(DEBUG, "api", "failed to get advertising data time"); |
| return std::nullopt; |
| } |
| out.last_updated = peer.le()->parsed_advertising_data_timestamp().value(); |
| |
| bt::BufferView data_view = peer.le()->advertising_data(); |
| std::optional<pw::multibuf::MultiBuf> data = |
| allocator.Allocate(data_view.size()); |
| if (!data) { |
| bt_log(DEBUG, "api", "failed to allocate buffer for advertising data"); |
| return std::nullopt; |
| } |
| StatusWithSize copy_status = data->CopyFrom(data_view.subspan()); |
| if (!copy_status.ok()) { |
| bt_log(DEBUG, |
| "api", |
| "failed to copy scan result data: %s", |
| copy_status.status().str()); |
| return std::nullopt; |
| } |
| out.data = std::move(data.value()); |
| |
| if (peer.name().has_value()) { |
| out.name.emplace(); |
| Status append_status = |
| pw::string::Append(out.name.value(), peer.name().value()); |
| // RESOURCE_EXHAUSTED means that the name was truncated, which is OK. |
| if (!append_status.ok() && !append_status.IsResourceExhausted()) { |
| bt_log(DEBUG, |
| "api", |
| "failed to set scan result name: %s", |
| append_status.str()); |
| return std::nullopt; |
| } |
| } |
| |
| return out; |
| } |
| |
| } // namespace |
| |
| Central::Central(bt::gap::Adapter::WeakPtr adapter, |
| pw::async::Dispatcher& dispatcher, |
| pw::multibuf::MultiBufAllocator& allocator) |
| : adapter_(std::move(adapter)), |
| dispatcher_(dispatcher), |
| heap_dispatcher_(dispatcher), |
| allocator_(allocator), |
| weak_factory_(this), |
| self_(weak_factory_.GetWeakPtr()) {} |
| |
| Central::~Central() { |
| std::lock_guard guard(lock()); |
| scans_.clear(); |
| } |
| |
| async2::OnceReceiver<Central::ConnectResult> Central::Connect( |
| pw::bluetooth::PeerId peer_id, |
| bluetooth::low_energy::Connection2::ConnectionOptions options) { |
| bt::PeerId internal_peer_id(peer_id); |
| bt::gap::LowEnergyConnectionOptions connection_options = |
| internal::ConnectionOptionsFrom(options); |
| |
| auto [result_sender, result_receiver] = |
| async2::MakeOnceSenderAndReceiver<ConnectResult>(); |
| |
| bt::gap::Adapter::LowEnergy::ConnectionResultCallback result_cb = |
| [self = self_, |
| peer = internal_peer_id, |
| sender = std::move(result_sender)]( |
| bt::gap::Adapter::LowEnergy::ConnectionResult result) mutable { |
| if (!self.is_alive()) { |
| return; |
| } |
| self->OnConnectionResult(peer, std::move(result), std::move(sender)); |
| }; |
| |
| async::TaskFunction task_fn = [self = self_, |
| internal_peer_id, |
| connection_options, |
| cb = std::move(result_cb)]( |
| async::Context&, Status status) mutable { |
| if (!status.ok() || !self.is_alive()) { |
| return; |
| } |
| self->adapter_->le()->Connect( |
| internal_peer_id, std::move(cb), connection_options); |
| }; |
| Status post_status = heap_dispatcher_.Post(std::move(task_fn)); |
| PW_CHECK_OK(post_status); |
| |
| return std::move(result_receiver); |
| } |
| |
| async2::OnceReceiver<Central::ScanStartResult> Central::Scan( |
| const ScanOptions& options) { |
| // TODO: https://pwbug.dev/377301546 - Support the different types of active |
| // scans. |
| bool active = (options.scan_type != ScanType::kPassive); |
| |
| if (options.filters.empty()) { |
| return async2::OnceReceiver<ScanStartResult>( |
| pw::unexpected(StartScanError::kInvalidParameters)); |
| } |
| |
| auto [result_sender, result_receiver] = |
| async2::MakeOnceSenderAndReceiver<Central::ScanStartResult>(); |
| |
| auto callback = |
| [self = self_, sender = std::move(result_sender)]( |
| std::unique_ptr<bt::gap::LowEnergyDiscoverySession> session) mutable { |
| // callback will always be run on the Bluetooth thread |
| |
| if (!self.is_alive()) { |
| sender.emplace(pw::unexpected(StartScanError::kInternal)); |
| return; |
| } |
| |
| if (!session) { |
| bt_log(WARN, "api", "failed to start LE discovery session"); |
| sender.emplace(pw::unexpected(StartScanError::kInternal)); |
| return; |
| } |
| |
| ScanHandleImpl* scan_handle_raw_ptr = |
| new ScanHandleImpl(session->scan_id(), &self.get()); |
| ScanHandle::Ptr scan_handle_ptr(scan_handle_raw_ptr); |
| { |
| std::lock_guard guard(lock()); |
| auto [iter, emplaced] = self->scans_.try_emplace(session->scan_id(), |
| std::move(session), |
| scan_handle_raw_ptr, |
| session->scan_id(), |
| &self.get()); |
| PW_CHECK(emplaced); |
| } |
| |
| sender.emplace(std::move(scan_handle_ptr)); |
| }; |
| |
| // Convert options to filters now because options contains non-owning views |
| // that won't be valid in callbacks. |
| std::vector<bt::hci::DiscoveryFilter> discovery_filters; |
| discovery_filters.reserve(options.filters.size()); |
| for (const ScanFilter& filter : options.filters) { |
| discovery_filters.emplace_back(DiscoveryFilterFrom(filter)); |
| } |
| |
| async::TaskFunction task_fn = [self = self_, |
| filters = std::move(discovery_filters), |
| active, |
| cb = std::move(callback)]( |
| async::Context&, Status status) mutable { |
| if (status.ok()) { |
| // TODO: https://pwbug.dev/377301546 - Support configuring interval, |
| // window, and PHY. |
| self->adapter_->le()->StartDiscovery(active, filters, std::move(cb)); |
| } |
| }; |
| Status post_status = heap_dispatcher_.Post(std::move(task_fn)); |
| PW_CHECK_OK(post_status); |
| |
| return std::move(result_receiver); |
| } |
| |
| pw::sync::Mutex& Central::lock() { return g_peripheral_lock; } |
| |
| Central::ScanHandleImpl::~ScanHandleImpl() { |
| std::lock_guard guard(lock()); |
| if (central_) { |
| central_->StopScanLocked(scan_id_); |
| } |
| } |
| |
| void Central::ScanHandleImpl::QueueScanResultLocked(ScanResult&& result) { |
| if (results_.size() == kMaxScanResultsQueueSize) { |
| results_.pop(); |
| } |
| results_.push(std::move(result)); |
| std::move(waker_).Wake(); |
| } |
| |
| async2::Poll<pw::Result<Central::ScanResult>> |
| Central::ScanHandleImpl::PendResult(async2::Context& cx) { |
| std::lock_guard guard(lock()); |
| if (!results_.empty()) { |
| ScanResult result = std::move(results_.front()); |
| results_.pop(); |
| return async2::Ready(std::move(result)); |
| } |
| |
| if (!central_) { |
| return async2::Ready(pw::Status::Cancelled()); |
| } |
| |
| PW_ASYNC_STORE_WAKER(cx, waker_, "scan result"); |
| return async2::Pending(); |
| } |
| |
| Central::ScanState::ScanState( |
| std::unique_ptr<bt::gap::LowEnergyDiscoverySession> session, |
| ScanHandleImpl* scan_handle, |
| uint16_t scan_id, |
| Central* central) |
| : scan_id_(scan_id), |
| scan_handle_(scan_handle), |
| central_(central), |
| session_(std::move(session)) { |
| session_->SetResultCallback( |
| [this](const bt::gap::Peer& peer) { OnScanResult(peer); }); |
| session_->set_error_callback([this]() { OnError(); }); |
| } |
| |
| Central::ScanState::~ScanState() { |
| // lock() is expected to be held |
| if (scan_handle_) { |
| scan_handle_->OnScanErrorLocked(); |
| scan_handle_ = nullptr; |
| } |
| } |
| |
| void Central::ScanState::OnScanResult(const bt::gap::Peer& peer) { |
| // TODO: https://pwbug.dev/377301546 - Getting only a Peer as a scan result is |
| // awkward. Update LowEnergyDiscoverySession to give us the actual |
| // LowEnergyScanResult. |
| std::lock_guard guard(lock()); |
| if (!scan_handle_) { |
| return; |
| } |
| |
| std::optional<Central::ScanResult> scan_result = |
| ScanResultFrom(peer, central_->allocator_); |
| if (!scan_result.has_value()) { |
| return; |
| } |
| |
| scan_handle_->QueueScanResultLocked(std::move(scan_result.value())); |
| scan_handle_->WakeLocked(); |
| } |
| |
| void Central::ScanState::OnError() { |
| std::lock_guard guard(lock()); |
| if (scan_handle_) { |
| scan_handle_->OnScanErrorLocked(); |
| scan_handle_ = nullptr; |
| } |
| central_->scans_.erase(scan_id_); |
| // This object has been destroyed. |
| } |
| |
| void Central::StopScanLocked(uint16_t scan_id) { |
| auto iter = scans_.find(scan_id); |
| if (iter == scans_.end()) { |
| return; |
| } |
| iter->second.OnScanHandleDestroyedLocked(); |
| |
| pw::Status post_status = heap_dispatcher_.Post( |
| [self = self_, scan_id](pw::async::Context, pw::Status status) { |
| if (!status.ok() || !self.is_alive()) { |
| return; |
| } |
| std::lock_guard guard(lock()); |
| self->scans_.erase(scan_id); |
| }); |
| PW_CHECK(post_status.ok()); |
| } |
| |
| void Central::OnConnectionResult( |
| bt::PeerId peer_id, |
| bt::gap::Adapter::LowEnergy::ConnectionResult result, |
| async2::OnceSender<ConnectResult> result_sender) { |
| if (result.is_error()) { |
| if (result.error_value() == bt::HostError::kNotFound) { |
| result_sender.emplace(pw::unexpected(ConnectError::kUnknownPeer)); |
| } else { |
| result_sender.emplace( |
| pw::unexpected(ConnectError::kCouldNotBeEstablished)); |
| } |
| return; |
| } |
| |
| pw::bluetooth::low_energy::Connection2::Ptr connection_ptr( |
| new internal::Connection( |
| peer_id, std::move(result.value()), dispatcher_)); |
| result_sender.emplace(std::move(connection_ptr)); |
| } |
| |
| } // namespace pw::bluetooth_sapphire |