// 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/fuchsia/host/fidl/gatt2_client_server.h" #include #include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" namespace fb = fuchsia::bluetooth; namespace fbg = fuchsia::bluetooth::gatt2; namespace bthost { namespace { fbg::ServiceInfo RemoteServiceToFidlServiceInfo( const bt::gatt::RemoteService::WeakPtr& svc) { fbg::ServiceInfo out; out.set_handle(fbg::ServiceHandle{svc->handle()}); auto kind = svc->info().kind == bt::gatt::ServiceKind::PRIMARY ? fbg::ServiceKind::PRIMARY : fbg::ServiceKind::SECONDARY; out.set_kind(kind); out.set_type(fb::Uuid{svc->uuid().value()}); return out; } } // namespace Gatt2ClientServer::Gatt2ClientServer( bt::gatt::PeerId peer_id, bt::gatt::GATT::WeakPtr weak_gatt, fidl::InterfaceRequest request, fit::callback error_cb) : GattServerBase(std::move(weak_gatt), /*impl=*/this, std::move(request)), peer_id_(peer_id), server_error_cb_(std::move(error_cb)), weak_self_(this) { set_error_handler([this](zx_status_t) { if (server_error_cb_) { server_error_cb_(); } }); // It is safe to bind |this| to the callback because the service watcher is // unregistered in the destructor. service_watcher_id_ = gatt()->RegisterRemoteServiceWatcherForPeer( peer_id_, [this](auto removed, auto added, auto modified) { // Ignore results before the initial call to ListServices() completes to // avoid redundant notifications. if (!list_services_complete_) { bt_log(TRACE, "fidl", "ignoring service watcher update before ListServices() result " "received"); return; } OnWatchServicesResult(removed, added, modified); }); } Gatt2ClientServer::~Gatt2ClientServer() { PW_CHECK(gatt()->UnregisterRemoteServiceWatcher(service_watcher_id_)); } void Gatt2ClientServer::OnWatchServicesResult( const std::vector& removed, const bt::gatt::ServiceList& added, const bt::gatt::ServiceList& modified) { // Accumulate all removed services and send in next result. if (!next_watch_services_result_.has_value()) { next_watch_services_result_.emplace(); } next_watch_services_result_->removed.insert(removed.begin(), removed.end()); // Remove any stale updated services (to avoid sending an invalid one to the // client). for (bt::att::Handle handle : removed) { next_watch_services_result_->updated.erase(handle); } // Replace any existing updated services with same handle and add new updates. for (const bt::gatt::RemoteService::WeakPtr& svc : added) { next_watch_services_result_->updated[svc->handle()] = svc; } for (const bt::gatt::RemoteService::WeakPtr& svc : modified) { next_watch_services_result_->updated[svc->handle()] = svc; } bt_log(TRACE, "fidl", "next watch services result: (removed: %zu, updated: %zu) (peer: %s)", next_watch_services_result_->removed.size(), next_watch_services_result_->updated.size(), bt_str(peer_id_)); TrySendNextWatchServicesResult(); } void Gatt2ClientServer::TrySendNextWatchServicesResult() { if (!watch_services_request_ || !next_watch_services_result_) { return; } std::vector fidl_removed; std::transform( next_watch_services_result_->removed.begin(), next_watch_services_result_->removed.end(), std::back_inserter(fidl_removed), [](const bt::att::Handle& handle) { return fbg::Handle{handle}; }); // Don't filter removed services by UUID because we don't know the UUIDs of // these services currently. // TODO(fxbug.dev/42111895): Filter removed services by UUID. std::vector fidl_updated; for (const ServiceMap::value_type& svc_pair : next_watch_services_result_->updated) { // Filter updated services by UUID. // NOTE: If clients change UUIDs they are requesting across requests, they // won't receive existing service with the new UUIDs, only new ones if (prev_watch_services_uuids_.empty() || prev_watch_services_uuids_.count(svc_pair.second->uuid()) == 1) { fidl_updated.push_back(RemoteServiceToFidlServiceInfo(svc_pair.second)); } } next_watch_services_result_.reset(); // Skip sending results that are empty after filtering services by UUID. if (fidl_removed.empty() && fidl_updated.empty()) { bt_log(TRACE, "fidl", "skipping service watcher update without matching UUIDs (peer: %s)", bt_str(peer_id_)); return; } // TODO(fxbug.dev/42165836): Use measure-tape to verify response fits in FIDL // channel before sending. This is only an issue for peers with very large // databases. bt_log(TRACE, "fidl", "notifying WatchServices() callback (removed: %zu, updated: %zu, " "peer: %s)", fidl_removed.size(), fidl_updated.size(), bt_str(peer_id_)); watch_services_request_.value()(std::move(fidl_updated), std::move(fidl_removed)); watch_services_request_.reset(); } // TODO(fxbug.dev/42165818): Do not send privileged services (e.g. Generic // Attribute Profile Service) to clients. void Gatt2ClientServer::WatchServices(std::vector fidl_uuids, WatchServicesCallback callback) { std::unordered_set uuids; std::transform(fidl_uuids.begin(), fidl_uuids.end(), std::inserter(uuids, uuids.begin()), [](const fb::Uuid& uuid) { return bt::UUID(uuid.value); }); // If the UUID filter list is changed between requests, perform a fresh // ListServices() call to ensure existing services that match the new UUIDs // are reported to the client. Dropping the old watch_services_request_ with // no new results. if (uuids != prev_watch_services_uuids_) { bt_log(DEBUG, "fidl", "WatchServices: UUIDs changed from previous call (peer: %s)", bt_str(peer_id_)); list_services_complete_ = false; // Clear old watch service results as we're about to get a fresh list of // services. next_watch_services_result_.reset(); prev_watch_services_uuids_ = uuids; if (watch_services_request_) { watch_services_request_.value()({}, {}); watch_services_request_.reset(); } } // Only allow 1 callback at a time. Close the server if this is violated. if (watch_services_request_) { bt_log(WARN, "fidl", "%s: call received while previous call is still pending", __FUNCTION__); binding()->Close(ZX_ERR_ALREADY_BOUND); server_error_cb_(); return; } watch_services_request_.emplace(std::move(callback)); auto self = weak_self_.GetWeakPtr(); // Return a complete service snapshot on the first call, or on calls that use // a new UUID filter list. if (!list_services_complete_) { std::vector uuids_vector(uuids.begin(), uuids.end()); gatt()->ListServices( peer_id_, std::move(uuids_vector), [self](bt::att::Result<> status, const bt::gatt::ServiceList& services) { if (!self.is_alive()) { return; } if (bt_is_error(status, INFO, "fidl", "WatchServices: ListServices failed (peer: %s)", bt_str(self->peer_id_))) { self->binding()->Close(ZX_ERR_CONNECTION_RESET); self->server_error_cb_(); return; } bt_log(DEBUG, "fidl", "WatchServices: ListServices complete (peer: %s)", bt_str(self->peer_id_)); PW_CHECK(self->watch_services_request_); self->list_services_complete_ = true; self->OnWatchServicesResult( /*removed=*/{}, /*added=*/services, /*modified=*/{}); }); return; } TrySendNextWatchServicesResult(); } void Gatt2ClientServer::ConnectToService( fbg::ServiceHandle handle, fidl::InterfaceRequest request) { bt_log(DEBUG, "fidl", "%s: (handle: 0x%lX)", __FUNCTION__, handle.value); if (!fidl_helpers::IsFidlGattServiceHandleValid(handle)) { request.Close(ZX_ERR_INVALID_ARGS); return; } bt::att::Handle service_handle = static_cast(handle.value); // Only allow clients to have 1 RemoteService per service at a time to prevent // race conditions between multiple RemoteService clients modifying a service, // and to simplify implementation. A client shouldn't need more than 1 // RemoteService per service at a time, but if they really need to, they can // create multiple Client instances. if (services_.count(service_handle) == 1) { request.Close(ZX_ERR_ALREADY_EXISTS); return; } // Mark this connection as in progress. services_.try_emplace(service_handle, nullptr); bt::gatt::RemoteService::WeakPtr service = gatt()->FindService(peer_id_, service_handle); if (!service.is_alive()) { bt_log(INFO, "fidl", "service not found (peer: %s, handle: %#.4x)", bt_str(peer_id_), service_handle); services_.erase(service_handle); request.Close(ZX_ERR_NOT_FOUND); return; } PW_CHECK(service_handle == service->handle()); // This removed handler may be called long after the service is removed from // the service map or this server is destroyed, since removed handlers are not // unregistered. If the FIDL client connects->disconnects->connects, it is // possible for this handler to be called twice (the second call should then // do nothing). auto self = weak_self_.GetWeakPtr(); fit::closure removed_handler = [self, service_handle] { if (!self.is_alive()) { return; } bt_log(DEBUG, "fidl", "service removed (peer: %s, handle: %#.4x)", bt_str(self->peer_id_), service_handle); auto svc_iter = self->services_.find(service_handle); if (svc_iter == self->services_.end()) { bt_log(TRACE, "fidl", "ignoring service removed callback for already removed service " "(peer: %s, handle: " "%#.4x)", bt_str(self->peer_id_), service_handle); return; } svc_iter->second->Close(ZX_ERR_CONNECTION_RESET); self->services_.erase(svc_iter); }; // The only reason RemoteService::AddRemovedHandler() can fail is if the // service is already shut down, but that should not be possible in this // synchronous callback (the service would not have been returned in the first // place). PW_CHECK(service->AddRemovedHandler(std::move(removed_handler)), "adding service removed handler failed (service may be shut " "down) (peer: %s, " "handle: %#.4x)", bt_str(peer_id_), service_handle); std::unique_ptr remote_service_server = std::make_unique( std::move(service), gatt(), peer_id_, std::move(request)); // Even if there is already an error, this handler won't be called until the // next yield to the event loop. remote_service_server->set_error_handler( [self, service_handle](zx_status_t status) { bt_log(TRACE, "fidl", "FIDL channel error (peer: %s, handle: %#.4x)", bt_str(self->peer_id_), service_handle); self->services_.erase(service_handle); }); // Error handler should not have been called yet. PW_CHECK(services_.count(service_handle) == 1); services_[service_handle] = std::move(remote_service_server); } } // namespace bthost