blob: 7788e3fdd86a49afef2f96406918c48a842c3aa3 [file] [log] [blame]
// Copyright 2017 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 "low_energy_central_server.h"
#include <zircon/types.h>
#include <utility>
#include "gatt_client_server.h"
#include "helpers.h"
#include "src/connectivity/bluetooth/core/bt-host/fidl/measure_tape/hlcpp_measure_tape_for_peer.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/assert.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/error.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/log.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/hci-spec/constants.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/sm/types.h"
using fuchsia::bluetooth::ErrorCode;
using fuchsia::bluetooth::Int8;
using fuchsia::bluetooth::Status;
using bt::sm::BondableMode;
using fuchsia::bluetooth::gatt::Client;
using fuchsia::bluetooth::le::ScanFilterPtr;
namespace fble = fuchsia::bluetooth::le;
namespace measure_fble = measure_tape::fuchsia::bluetooth::le;
namespace bthost {
namespace {
bt::gap::LowEnergyConnectionOptions ConnectionOptionsFromFidl(
const fble::ConnectionOptions& options) {
BondableMode bondable_mode = (!options.has_bondable_mode() || options.bondable_mode())
? BondableMode::Bondable
: BondableMode::NonBondable;
std::optional<bt::UUID> service_uuid =
options.has_service_filter()
? std::optional(fidl_helpers::UuidFromFidl(options.service_filter()))
: std::nullopt;
return bt::gap::LowEnergyConnectionOptions{.bondable_mode = bondable_mode,
.service_uuid = service_uuid};
}
} // namespace
LowEnergyCentralServer::LowEnergyCentralServer(bt::gap::Adapter::WeakPtr adapter,
fidl::InterfaceRequest<Central> request,
bt::gatt::GATT::WeakPtr gatt)
: AdapterServerBase(std::move(adapter), this, std::move(request)),
gatt_(std::move(gatt)),
requesting_scan_deprecated_(false),
weak_self_(this) {
BT_ASSERT(gatt_.is_alive());
}
LowEnergyCentralServer::~LowEnergyCentralServer() {
if (scan_instance_) {
scan_instance_->Close(ZX_OK);
scan_instance_.reset();
}
}
std::optional<bt::gap::LowEnergyConnectionHandle*> LowEnergyCentralServer::FindConnectionForTesting(
bt::PeerId identifier) {
auto conn_iter = connections_deprecated_.find(identifier);
if (conn_iter != connections_deprecated_.end()) {
return conn_iter->second.get();
}
return std::nullopt;
}
LowEnergyCentralServer::ScanResultWatcherServer::ScanResultWatcherServer(
bt::gap::Adapter::WeakPtr adapter,
fidl::InterfaceRequest<fuchsia::bluetooth::le::ScanResultWatcher> watcher,
fit::callback<void()> error_cb)
: ServerBase(this, std::move(watcher)),
adapter_(std::move(adapter)),
error_callback_(std::move(error_cb)) {
set_error_handler([this](auto) {
bt_log(DEBUG, "fidl", "ScanResultWatcher client closed, stopping scan");
BT_ASSERT(error_callback_);
error_callback_();
});
}
void LowEnergyCentralServer::ScanResultWatcherServer::Close(zx_status_t epitaph) {
binding()->Close(epitaph);
}
void LowEnergyCentralServer::ScanResultWatcherServer::AddPeers(
std::unordered_set<bt::PeerId> peers) {
while (!peers.empty() && updated_peers_.size() < kMaxPendingScanResultWatcherPeers) {
updated_peers_.insert(peers.extract(peers.begin()));
}
if (!peers.empty()) {
bt_log(WARN, "fidl", "Maximum pending peers (%zu) reached, dropping %zu peers from results",
kMaxPendingScanResultWatcherPeers, peers.size());
}
MaybeSendPeers();
}
void LowEnergyCentralServer::ScanResultWatcherServer::Watch(WatchCallback callback) {
bt_log(TRACE, "fidl", "%s", __FUNCTION__);
if (watch_callback_) {
bt_log(WARN, "fidl", "%s: called before previous call completed", __FUNCTION__);
Close(ZX_ERR_CANCELED);
BT_ASSERT(error_callback_);
error_callback_();
return;
}
watch_callback_ = std::move(callback);
MaybeSendPeers();
}
void LowEnergyCentralServer::ScanResultWatcherServer::MaybeSendPeers() {
if (updated_peers_.empty() || !watch_callback_) {
return;
}
// Send as many peers as will fit in the channel.
const size_t kVectorOverhead = sizeof(fidl_message_header_t) + sizeof(fidl_vector_t);
const size_t kMaxBytes = ZX_CHANNEL_MAX_MSG_BYTES - kVectorOverhead;
size_t bytes_used = 0;
std::vector<fble::Peer> peers;
while (!updated_peers_.empty()) {
bt::PeerId peer_id = *updated_peers_.begin();
bt::gap::Peer* peer = adapter_->peer_cache()->FindById(peer_id);
if (!peer) {
// The peer has been removed from the peer cache since it was queued, so the stale peer ID
// should not be sent to the client.
updated_peers_.erase(peer_id);
continue;
}
fble::Peer fidl_peer = fidl_helpers::PeerToFidlLe(*peer);
measure_fble::Size peer_size = measure_fble::Measure(fidl_peer);
BT_ASSERT_MSG(peer_size.num_handles == 0,
"Expected fuchsia.bluetooth.le/Peer to not have handles, but %zu handles found",
peer_size.num_handles);
bytes_used += peer_size.num_bytes;
if (bytes_used > kMaxBytes) {
// Don't remove the peer that exceeded the size limit. It will be sent in the next batch.
break;
}
updated_peers_.erase(peer_id);
peers.emplace_back(std::move(fidl_peer));
}
// It is possible that all queued peers were stale, so there is nothing to send.
if (peers.empty()) {
return;
}
watch_callback_(std::move(peers));
}
LowEnergyCentralServer::ScanInstance::ScanInstance(
bt::gap::Adapter::WeakPtr adapter, LowEnergyCentralServer* central_server,
std::vector<fuchsia::bluetooth::le::Filter> fidl_filters,
fidl::InterfaceRequest<fuchsia::bluetooth::le::ScanResultWatcher> watcher, ScanCallback cb)
: result_watcher_(adapter, std::move(watcher),
/*error_cb=*/
[this] {
Close(ZX_OK);
central_server_->ClearScan();
}),
scan_complete_callback_(std::move(cb)),
central_server_(central_server),
adapter_(std::move(adapter)),
weak_self_(this) {
std::transform(fidl_filters.begin(), fidl_filters.end(), std::back_inserter(filters_),
fidl_helpers::DiscoveryFilterFromFidl);
// Send all current peers in peer cache that match filters.
std::unordered_set<bt::PeerId> initial_peers;
adapter_->peer_cache()->ForEach(
[&](const bt::gap::Peer& peer) { initial_peers.emplace(peer.identifier()); });
FilterAndAddPeers(std::move(initial_peers));
// Subscribe to updated peers.
peer_updated_callback_id_ = adapter_->peer_cache()->add_peer_updated_callback(
[this](const bt::gap::Peer& peer) { FilterAndAddPeers({peer.identifier()}); });
auto self = weak_self_.GetWeakPtr();
adapter_->le()->StartDiscovery(
/*active=*/true, [self](auto session) {
if (!self.is_alive()) {
bt_log(TRACE, "fidl", "ignoring LE discovery session for canceled Scan");
return;
}
if (!session) {
bt_log(WARN, "fidl", "failed to start LE discovery session");
self->Close(ZX_ERR_INTERNAL);
self->central_server_->ClearScan();
return;
}
session->set_error_callback([self] {
if (!self.is_alive()) {
bt_log(TRACE, "fidl", "ignoring LE discovery session error for canceled Scan");
return;
}
bt_log(DEBUG, "fidl", "canceling Scan due to LE discovery session error");
self->Close(ZX_ERR_INTERNAL);
self->central_server_->ClearScan();
});
self->scan_session_ = std::move(session);
});
}
LowEnergyCentralServer::ScanInstance::~ScanInstance() {
// If this scan instance has not already been closed with a more specific status, close with an
// error status.
Close(ZX_ERR_INTERNAL);
adapter_->peer_cache()->remove_peer_updated_callback(peer_updated_callback_id_);
}
void LowEnergyCentralServer::ScanInstance::Close(zx_status_t status) {
if (scan_complete_callback_) {
result_watcher_.Close(status);
scan_complete_callback_();
}
}
void LowEnergyCentralServer::ScanInstance::FilterAndAddPeers(std::unordered_set<bt::PeerId> peers) {
// Remove peers that don't match any filters.
for (auto peers_iter = peers.begin(); peers_iter != peers.end();) {
bt::gap::Peer* peer = adapter_->peer_cache()->FindById(*peers_iter);
if (!peer || !peer->le()) {
peers_iter = peers.erase(peers_iter);
continue;
}
bool matches_any = false;
for (const bt::gap::DiscoveryFilter& filter : filters_) {
// TODO(https://fxbug.dev/42111894): Match peer names that are not in advertising data.
// This might require implementing a new peer filtering class, as
// DiscoveryFilter only filters advertising data.
if (filter.MatchLowEnergyResult(peer->le()->parsed_advertising_data(), peer->connectable(),
peer->rssi())) {
matches_any = true;
break;
}
}
if (!matches_any) {
peers_iter = peers.erase(peers_iter);
continue;
}
peers_iter++;
}
result_watcher_.AddPeers(std::move(peers));
}
void LowEnergyCentralServer::Scan(
fuchsia::bluetooth::le::ScanOptions options,
fidl::InterfaceRequest<fuchsia::bluetooth::le::ScanResultWatcher> result_watcher,
ScanCallback callback) {
bt_log(DEBUG, "fidl", "%s", __FUNCTION__);
if (scan_instance_ || requesting_scan_deprecated_ || scan_session_deprecated_) {
bt_log(INFO, "fidl", "%s: scan already in progress", __FUNCTION__);
result_watcher.Close(ZX_ERR_ALREADY_EXISTS);
callback();
return;
}
if (!options.has_filters() || options.filters().empty()) {
bt_log(INFO, "fidl", "%s: no scan filters specified", __FUNCTION__);
result_watcher.Close(ZX_ERR_INVALID_ARGS);
callback();
return;
}
scan_instance_ = std::make_unique<ScanInstance>(adapter()->AsWeakPtr(), this,
std::move(*options.mutable_filters()),
std::move(result_watcher), std::move(callback));
}
void LowEnergyCentralServer::Connect(fuchsia::bluetooth::PeerId id, fble::ConnectionOptions options,
fidl::InterfaceRequest<fble::Connection> request) {
bt::PeerId peer_id(id.value);
bt_log(INFO, "fidl", "%s: (peer: %s)", __FUNCTION__, bt_str(peer_id));
auto conn_iter = connections_.find(peer_id);
if (conn_iter != connections_.end()) {
bt_log(INFO, "fidl", "%s: connection %s (peer: %s)", __FUNCTION__,
(conn_iter->second == nullptr ? "request pending" : "already exists"), bt_str(peer_id));
request.Close(ZX_ERR_ALREADY_BOUND);
return;
}
auto self = weak_self_.GetWeakPtr();
auto conn_cb = [self, peer_id, request = std::move(request)](
bt::gap::Adapter::LowEnergy::ConnectionResult result) mutable {
if (!self.is_alive())
return;
auto conn_iter = self->connections_.find(peer_id);
BT_ASSERT(conn_iter != self->connections_.end());
BT_ASSERT(conn_iter->second == nullptr);
if (result.is_error()) {
bt_log(INFO, "fidl", "Connect: failed to connect to peer (peer: %s)", bt_str(peer_id));
self->connections_.erase(peer_id);
request.Close(ZX_ERR_NOT_CONNECTED);
return;
}
auto conn_ref = std::move(result).value();
BT_ASSERT(conn_ref);
BT_ASSERT(peer_id == conn_ref->peer_identifier());
auto closed_cb = [self, peer_id] {
if (self.is_alive()) {
self->connections_.erase(peer_id);
}
};
auto server = std::make_unique<LowEnergyConnectionServer>(
self->gatt_, std::move(conn_ref), request.TakeChannel(), std::move(closed_cb));
BT_ASSERT(!conn_iter->second);
conn_iter->second = std::move(server);
};
// An entry for the connection must be created here so that a synchronous call to conn_cb below
// does not cause conn_cb to treat the connection as cancelled.
connections_[peer_id] = nullptr;
adapter()->le()->Connect(peer_id, std::move(conn_cb), ConnectionOptionsFromFidl(options));
}
void LowEnergyCentralServer::GetPeripherals(::fidl::VectorPtr<::std::string> service_uuids,
GetPeripheralsCallback callback) {
// TODO:
bt_log(ERROR, "fidl", "GetPeripherals() not implemented");
}
void LowEnergyCentralServer::GetPeripheral(::std::string identifier,
GetPeripheralCallback callback) {
// TODO:
bt_log(ERROR, "fidl", "GetPeripheral() not implemented");
}
void LowEnergyCentralServer::StartScan(ScanFilterPtr filter, StartScanCallback callback) {
bt_log(DEBUG, "fidl", "%s", __FUNCTION__);
if (requesting_scan_deprecated_) {
bt_log(DEBUG, "fidl", "%s: scan request already in progress", __FUNCTION__);
callback(fidl_helpers::NewFidlError(ErrorCode::IN_PROGRESS, "Scan request in progress"));
return;
}
if (filter && !fidl_helpers::IsScanFilterValid(*filter)) {
bt_log(WARN, "fidl", "%s: invalid scan filter given", __FUNCTION__);
callback(fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS,
"ScanFilter contains an invalid UUID"));
return;
}
if (scan_session_deprecated_) {
// A scan is already in progress. Update its filter and report success.
scan_session_deprecated_->filter()->Reset();
fidl_helpers::PopulateDiscoveryFilter(*filter, scan_session_deprecated_->filter());
callback(Status());
return;
}
requesting_scan_deprecated_ = true;
adapter()->le()->StartDiscovery(/*active=*/true, [self = weak_self_.GetWeakPtr(),
filter = std::move(filter),
callback = std::move(callback),
func = __FUNCTION__](auto session) {
if (!self.is_alive())
return;
self->requesting_scan_deprecated_ = false;
if (!session) {
bt_log(WARN, "fidl", "%s: failed to start LE discovery session", func);
callback(fidl_helpers::NewFidlError(ErrorCode::FAILED, "Failed to start discovery session"));
return;
}
// Assign the filter contents if a filter was provided.
if (filter)
fidl_helpers::PopulateDiscoveryFilter(*filter, session->filter());
session->SetResultCallback([self](const auto& peer) {
if (self.is_alive())
self->OnScanResult(peer);
});
session->set_error_callback([self] {
if (self.is_alive()) {
// Clean up the session and notify the delegate.
self->StopScan();
}
});
self->scan_session_deprecated_ = std::move(session);
self->NotifyScanStateChanged(true);
callback(Status());
});
}
void LowEnergyCentralServer::StopScan() {
bt_log(DEBUG, "fidl", "StopScan()");
if (!scan_session_deprecated_) {
bt_log(DEBUG, "fidl", "%s: no active discovery session; nothing to do", __FUNCTION__);
return;
}
scan_session_deprecated_ = nullptr;
NotifyScanStateChanged(false);
}
void LowEnergyCentralServer::ConnectPeripheral(
::std::string identifier, fuchsia::bluetooth::le::ConnectionOptions connection_options,
::fidl::InterfaceRequest<Client> client_request, ConnectPeripheralCallback callback) {
bt_log(INFO, "fidl", "%s: (peer: %s)", __FUNCTION__, identifier.c_str());
auto peer_id = fidl_helpers::PeerIdFromString(identifier);
if (!peer_id.has_value()) {
bt_log(WARN, "fidl", "%s: invalid peer id : %s", __FUNCTION__, identifier.c_str());
callback(fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, "invalid peer ID"));
return;
}
auto iter = connections_deprecated_.find(*peer_id);
if (iter != connections_deprecated_.end()) {
if (iter->second) {
bt_log(INFO, "fidl", "%s: already connected to %s", __FUNCTION__, bt_str(*peer_id));
callback(
fidl_helpers::NewFidlError(ErrorCode::ALREADY, "Already connected to requested peer"));
} else {
bt_log(INFO, "fidl", "%s: connect request pending (peer: %s)", __FUNCTION__,
bt_str(*peer_id));
callback(fidl_helpers::NewFidlError(ErrorCode::IN_PROGRESS, "Connect request pending"));
}
return;
}
auto self = weak_self_.GetWeakPtr();
auto conn_cb = [self, callback = callback.share(), peer_id = *peer_id,
request = std::move(client_request), func = __FUNCTION__](auto result) mutable {
if (!self.is_alive())
return;
auto iter = self->connections_deprecated_.find(peer_id);
if (iter == self->connections_deprecated_.end()) {
bt_log(INFO, "fidl", "%s: connect request canceled during connection procedure (peer: %s)",
func, bt_str(peer_id));
auto error = fidl_helpers::NewFidlError(ErrorCode::FAILED, "Connect request canceled");
callback(std::move(error));
return;
}
if (result.is_error()) {
bt_log(INFO, "fidl", "%s: failed to connect to peer (peer: %s)", func, bt_str(peer_id));
self->connections_deprecated_.erase(peer_id);
callback(fidl_helpers::ResultToFidlDeprecated(bt::ToResult(result.error_value()),
"failed to connect"));
return;
}
auto conn_ref = std::move(result).value();
BT_ASSERT(conn_ref);
BT_ASSERT(peer_id == conn_ref->peer_identifier());
if (self->gatt_client_servers_.find(peer_id) != self->gatt_client_servers_.end()) {
bt_log(WARN, "fidl", "only 1 gatt.Client FIDL handle allowed per peer (%s)", bt_str(peer_id));
// The handle owned by |request| will be closed.
return;
}
auto server = std::make_unique<GattClientServer>(peer_id, self->gatt_, std::move(request));
server->set_error_handler([self, peer_id](zx_status_t status) {
if (self.is_alive()) {
bt_log(DEBUG, "bt-host", "GATT client disconnected");
self->gatt_client_servers_.erase(peer_id);
}
});
self->gatt_client_servers_.emplace(peer_id, std::move(server));
conn_ref->set_closed_callback([self, peer_id] {
if (self.is_alive() && self->connections_deprecated_.erase(peer_id) != 0) {
bt_log(INFO, "fidl", "peripheral connection closed (peer: %s)", bt_str(peer_id));
self->gatt_client_servers_.erase(peer_id);
self->NotifyPeripheralDisconnected(peer_id);
}
});
BT_ASSERT(!iter->second);
iter->second = std::move(conn_ref);
callback(Status());
};
// An entry for the connection must be created here so that a synchronous call to conn_cb below
// does not cause conn_cb to treat the connection as cancelled.
connections_deprecated_[*peer_id] = nullptr;
adapter()->le()->Connect(*peer_id, std::move(conn_cb),
ConnectionOptionsFromFidl(connection_options));
}
void LowEnergyCentralServer::DisconnectPeripheral(::std::string identifier,
DisconnectPeripheralCallback callback) {
auto peer_id = fidl_helpers::PeerIdFromString(identifier);
if (!peer_id.has_value()) {
bt_log(WARN, "fidl", "%s: invalid peer id : %s", __FUNCTION__, identifier.c_str());
callback(fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, "invalid peer ID"));
return;
}
auto iter = connections_deprecated_.find(*peer_id);
if (iter == connections_deprecated_.end()) {
bt_log(INFO, "fidl", "%s: client not connected to peer (peer: %s)", __FUNCTION__,
identifier.c_str());
callback(Status());
return;
}
// If a request to this peer is pending then the request will be canceled.
bool was_pending = !iter->second;
connections_deprecated_.erase(iter);
if (was_pending) {
bt_log(INFO, "fidl", "%s: canceling connection request (peer: %s)", __FUNCTION__,
bt_str(*peer_id));
} else {
gatt_client_servers_.erase(*peer_id);
NotifyPeripheralDisconnected(*peer_id);
}
callback(Status());
}
void LowEnergyCentralServer::OnScanResult(const bt::gap::Peer& peer) {
auto fidl_device = fidl_helpers::NewLERemoteDevice(peer);
if (!fidl_device) {
return;
}
if (peer.rssi() != bt::hci_spec::kRSSIInvalid) {
fidl_device->rssi = std::make_unique<Int8>();
fidl_device->rssi->value = peer.rssi();
}
binding()->events().OnDeviceDiscovered(std::move(*fidl_device));
}
void LowEnergyCentralServer::NotifyScanStateChanged(bool scanning) {
binding()->events().OnScanStateChanged(scanning);
}
void LowEnergyCentralServer::NotifyPeripheralDisconnected(bt::PeerId peer_id) {
binding()->events().OnPeripheralDisconnected(peer_id.ToString());
}
} // namespace bthost