From ca60afa007b6b2b9efbef09c92025f1e85cd1354 Mon Sep 17 00:00:00 2001 From: Janusz Sobczak Date: Mon, 12 Jun 2023 18:28:49 -0700 Subject: [PATCH] Add robust gatt client PiperOrigin-RevId: 539817203 --- fastpair/internal/mediums/BUILD | 27 +- .../internal/mediums/robust_gatt_client.cc | 425 ++++++++++++ .../internal/mediums/robust_gatt_client.h | 272 ++++++++ .../mediums/robust_gatt_client_test.cc | 612 ++++++++++++++++++ internal/platform/implementation/BUILD | 2 + internal/platform/implementation/g3/ble_v2.cc | 1 + 6 files changed, 1338 insertions(+), 1 deletion(-) create mode 100644 fastpair/internal/mediums/robust_gatt_client.cc create mode 100644 fastpair/internal/mediums/robust_gatt_client.h create mode 100644 fastpair/internal/mediums/robust_gatt_client_test.cc diff --git a/fastpair/internal/mediums/BUILD b/fastpair/internal/mediums/BUILD index 3b43b962..561b6f1e 100644 --- a/fastpair/internal/mediums/BUILD +++ b/fastpair/internal/mediums/BUILD @@ -21,6 +21,7 @@ cc_library( "ble_v2.cc", "bluetooth_classic.cc", "bluetooth_radio.cc", + "robust_gatt_client.cc", ], hdrs = [ "ble.h", @@ -28,15 +29,20 @@ cc_library( "bluetooth_classic.h", "bluetooth_radio.h", "mediums.h", + "robust_gatt_client.h", ], visibility = [ "//fastpair:__subpackages__", ], deps = [ - "//internal/platform:base", + "//fastpair/common", "//internal/platform:comm", "//internal/platform:logging", "//internal/platform:types", + "//internal/platform/implementation:comm", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", ], @@ -125,3 +131,22 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_test( + name = "robust_gatt_client_test", + size = "small", + srcs = [ + "robust_gatt_client_test.cc", + ], + deps = [ + ":mediums", + "//internal/platform:comm", + "//internal/platform:test_util", + "//internal/platform:types", + "//internal/platform/implementation/g3", # build_cleaner: keep + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/status", + "@com_google_absl//absl/time", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/fastpair/internal/mediums/robust_gatt_client.cc b/fastpair/internal/mediums/robust_gatt_client.cc new file mode 100644 index 00000000..885db170 --- /dev/null +++ b/fastpair/internal/mediums/robust_gatt_client.cc @@ -0,0 +1,425 @@ +// Copyright 2023 Google LLC +// +// 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 "fastpair/internal/mediums/robust_gatt_client.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/functional/any_invocable.h" +#include "absl/status/status.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "internal/platform/single_thread_executor.h" + +namespace nearby { +namespace fastpair { +namespace {} // namespace + +RobustGattClient::~RobustGattClient() { + Stop(); + executor_.Shutdown(); +} + +void RobustGattClient::Stop() { + NEARBY_LOGS(INFO) << "Stopping gatt client"; + stopped_ = true; + executor_.Execute("cleanup", [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED( + executor_) { Cleanup(); }); +} + +void RobustGattClient::Connect() { + executor_.Execute("connect-gatt", + [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_) { + NEARBY_LOGS(INFO) << "Connecting to Gatt server"; + status_ = TryConnect(); + if (!status_.ok()) { + NEARBY_LOGS(INFO) << status_; + NotifyClient(status_); + return; + } + NEARBY_LOGS(INFO) << "Discovering services"; + status_ = TryDiscoverServices(); + if (!status_.ok()) { + NEARBY_LOGS(INFO) << status_; + NotifyClient(status_); + return; + } + NEARBY_LOGS(INFO) << "Gatt connection ready"; + NotifyClient(absl::OkStatus()); + }); +} + +absl::Status RobustGattClient::TryConnect() { + ExpBackOff back_off(params_); + absl::Time start_time = SystemClock().ElapsedRealtime(); + while (!stopped_ && SystemClock().ElapsedRealtime() - start_time < + params_.connect_timeout) { + gatt_client_ = medium_.ConnectToGattServer( + peripheral_, params_.tx_power_level, {.disconnected_cb = [this]() { + if (stopped_) return; + NEARBY_LOGS(INFO) << "Gatt server disconnected. Reconnecting..."; + Connect(); + }}); + if (gatt_client_ != nullptr && gatt_client_->IsValid()) { + return absl::OkStatus(); + } + SystemClock().Sleep(back_off.NextBackOff()); + } + return absl::DeadlineExceededError("gatt connection time-out"); +} + +absl::Status RobustGattClient::TryDiscoverServices() { + ExpBackOff back_off(params_); + absl::Time start_time = SystemClock().ElapsedRealtime(); + while (!stopped_ && SystemClock().ElapsedRealtime() - start_time < + params_.discovery_timeout) { + if (DiscoverServices(params_.service_uuid, + GetPrimaryCharacteristicList()) || + DiscoverServices(params_.service_uuid, + GetFallbackCharacteristicList())) { + return absl::OkStatus(); + } + SystemClock().Sleep(back_off.NextBackOff()); + } + return absl::DeadlineExceededError("gatt discovery time-out"); +} + +bool RobustGattClient::DiscoverServices( + const Uuid& service_uuid, const std::vector& characteristic_uuids) { + if (service_uuid.IsEmpty() || characteristic_uuids.empty()) { + return false; + } + return gatt_client_->DiscoverServiceAndCharacteristics(service_uuid, + characteristic_uuids); +} + +std::optional +RobustGattClient::GetCharacteristic(const Uuid& service_id, + const UuidPair& characteristic_uuid_pair) { + if (stopped_) return std::nullopt; + std::optional characteristic = + gatt_client_->GetCharacteristic(service_id, + characteristic_uuid_pair.primary_uuid); + if (characteristic.has_value()) { + return characteristic; + } + if (characteristic_uuid_pair.fallback_uuid.IsEmpty() || stopped_) { + return std::nullopt; + } + return gatt_client_->GetCharacteristic( + service_id, characteristic_uuid_pair.fallback_uuid); +} + +const RobustGattClient::GattCharacteristic* RobustGattClient::GetCharacteristic( + int uuid_pair_index) { + auto it = characteristics_.find(uuid_pair_index); + if (it != characteristics_.end()) { + return &it->second; + } + const UuidPair& uuid_pair = params_.characteristic_uuids[uuid_pair_index]; + std::optional characteristic = + GetCharacteristic(params_.service_uuid, uuid_pair); + if (!characteristic.has_value()) { + NEARBY_LOGS(WARNING) << absl::StrFormat( + "Characteristic (%s, %s) not found on service %s", + std::string(uuid_pair.primary_uuid), + std::string(uuid_pair.fallback_uuid), + std::string(params_.service_uuid)); + } + characteristics_[uuid_pair_index] = *characteristic; + return &characteristics_[uuid_pair_index]; +} + +std::vector RobustGattClient::GetPrimaryCharacteristicList() { + std::vector result; + result.reserve(params_.characteristic_uuids.size()); + for (auto& uuid_pair : params_.characteristic_uuids) { + result.push_back(uuid_pair.primary_uuid); + } + return result; +} + +std::vector RobustGattClient::GetFallbackCharacteristicList() { + bool has_fallbacks = false; + std::vector result; + result.reserve(params_.characteristic_uuids.size()); + for (auto& uuid_pair : params_.characteristic_uuids) { + if (uuid_pair.fallback_uuid.IsEmpty()) { + // Some characteristics may have only one, primary UUID. + result.push_back(uuid_pair.primary_uuid); + } else { + has_fallbacks = true; + result.push_back(uuid_pair.fallback_uuid); + } + } + if (!has_fallbacks) result.clear(); + return result; +} + +void RobustGattClient::WriteCharacteristic( + int uuid_pair_index, absl::string_view value, + api::ble_v2::GattClient::WriteType write_type, WriteCallback callback) { + CHECK_LT(uuid_pair_index, params_.characteristic_uuids.size()); + Write({.uuid_pair_index = uuid_pair_index, + .value = std::string(value), + .write_type = write_type, + .callback = std::move(callback), + .time_left = params_.gatt_operation_timeout, + .back_off = ExpBackOff(params_)}); +} + +void RobustGattClient::CallRemoteFunction(int uuid_pair_index, + absl::string_view request, + NotifyCallback response) { + CHECK_LT(uuid_pair_index, params_.characteristic_uuids.size()); + Subscribe(uuid_pair_index, std::move(response), /*call_once=*/true); + WriteCharacteristic(uuid_pair_index, request, + api::ble_v2::GattClient::WriteType::kWithResponse, + [this, uuid_pair_index](absl::Status result) { + if (!result.ok()) { + NotifySubscriber(uuid_pair_index, result); + } else { + StartNotifyTimer(uuid_pair_index); + } + }); +} + +void RobustGattClient::StartNotifyTimer(int uuid_pair_index) { + if (stopped_) return; + MutexLock lock(&mutex_); + auto it = notify_callbacks_.find(uuid_pair_index); + if (it == notify_callbacks_.end()) return; + it->second.timer = std::make_unique(); + it->second.timer->Start( + absl::ToInt64Milliseconds(params_.gatt_operation_timeout), 0, + [this, uuid_pair_index]() { + NotifySubscriber(uuid_pair_index, + absl::DeadlineExceededError("gatt operation timeout")); + }); +} + +void RobustGattClient::Write(WriteRequest request) { + executor_.Execute( + "write-gatt", + [this, request = std::move(request)]() + ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_) mutable { + absl::Time start_time = SystemClock::ElapsedRealtime(); + if (stopped_) return; + if (!status_.ok()) { + NEARBY_LOGS(WARNING) + << "Cannot write due to connection error " << status_; + std::move(request.callback)(status_); + return; + } + SystemClock().Sleep(request.back_off.NextBackOff()); + const GattCharacteristic* characteristic = + GetCharacteristic(request.uuid_pair_index); + bool result = false; + if (characteristic != nullptr) { + result = gatt_client_->WriteCharacteristic( + *characteristic, request.value, request.write_type); + } + if (stopped_) return; + if (result) { + std::move(request.callback)(absl::OkStatus()); + } else { + request.time_left -= SystemClock::ElapsedRealtime() - start_time; + if (request.time_left > absl::ZeroDuration()) { + Write(std::move(request)); + } else { + std::move(request.callback)( + absl::UnavailableError("gatt write failed")); + } + } + }); +} + +void RobustGattClient::Subscribe(int uuid_pair_index, NotifyCallback callback, + bool call_once) { + CHECK_LT(uuid_pair_index, params_.characteristic_uuids.size()); + MutexLock lock(&mutex_); + notify_callbacks_[uuid_pair_index] = NotifyCallbackInfo{ + .callback = std::move(callback), + .call_once = call_once, + }; + NEARBY_LOGS(INFO) << "Subscribe to characteristic no: " << uuid_pair_index; + Subscribe(SubscribeRequest{ + .uuid_pair_index = uuid_pair_index, + .time_left = params_.gatt_operation_timeout, + .back_off = ExpBackOff(params_), + }); +} + +void RobustGattClient::Subscribe(SubscribeRequest request) { + executor_.Execute( + "subscribe", + [this, request = std::move(request)]() + ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_) mutable { + if (stopped_) return; + if (!HasSubsriberCallback(request.uuid_pair_index)) return; + if (!status_.ok()) { + NotifySubscriber(request.uuid_pair_index, status_); + return; + } + absl::Time start_time = SystemClock::ElapsedRealtime(); + SystemClock().Sleep(request.back_off.NextBackOff()); + const GattCharacteristic* characteristic = + GetCharacteristic(request.uuid_pair_index); + bool result = false; + if (characteristic != nullptr) { + result = gatt_client_->SetCharacteristicSubscription( + *characteristic, true, + [this, uuid_pair_index = + request.uuid_pair_index](absl::string_view value) { + NotifySubscriber(uuid_pair_index, value); + }); + } + if (stopped_) return; + if (!result) { + request.time_left -= SystemClock::ElapsedRealtime() - start_time; + if (request.time_left > absl::ZeroDuration()) { + Subscribe(std::move(request)); + } else { + NotifySubscriber( + request.uuid_pair_index, + absl::UnavailableError("gatt subscription failed")); + } + } + }); +} + +void RobustGattClient::Unsubscribe(int uuid_pair_index) { + CHECK_LT(uuid_pair_index, params_.characteristic_uuids.size()); + MutexLock lock(&mutex_); + int removed = notify_callbacks_.erase(uuid_pair_index); + if (removed == 0) { + NEARBY_LOGS(VERBOSE) << "Not subscribed for characteristic no: " + << uuid_pair_index; + } + UnsubscribeInternal(uuid_pair_index); +} + +void RobustGattClient::UnsubscribeInternal(int uuid_pair_index) { + executor_.Execute( + "unsubscribe", + [this, uuid_pair_index]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_) { + if (stopped_) return; + if (!status_.ok()) { + return; + } + NEARBY_LOGS(INFO) << "Unsubscribe from characteristic no: " + << uuid_pair_index; + const GattCharacteristic* characteristic = + GetCharacteristic(uuid_pair_index); + if (characteristic != nullptr) { + gatt_client_->SetCharacteristicSubscription(*characteristic, false, + [](absl::string_view) {}); + } + }); +} + +bool RobustGattClient::HasSubsriberCallback(int uuid_pair_index) { + MutexLock lock(&mutex_); + return notify_callbacks_.find(uuid_pair_index) != notify_callbacks_.end(); +} + +void RobustGattClient::NotifySubscriber( + int uuid_pair_index, absl::StatusOr value) { + if (stopped_) return; + MutexLock lock(&mutex_); + auto it = notify_callbacks_.find(uuid_pair_index); + if (it != notify_callbacks_.end()) { + it->second.callback(value); + if (it->second.timer) { + it->second.timer->Stop(); + // NotifySubscriber could be called from the timer. We can't destroy the + // timer directly from the timer callback. + DestroyOnExecutor(std::move(it->second.timer), &executor_); + } + if (it->second.call_once) { + notify_callbacks_.erase(it); + UnsubscribeInternal(uuid_pair_index); + } + } +} + +void RobustGattClient::ReadCharacteristic(int uuid_pair_index, + ReadCallback callback) { + CHECK_LT(uuid_pair_index, params_.characteristic_uuids.size()); + Read(ReadRequest{ + .uuid_pair_index = uuid_pair_index, + .callback = std::move(callback), + .time_left = params_.gatt_operation_timeout, + .back_off = ExpBackOff(params_), + }); +} +void RobustGattClient::Read(ReadRequest request) { + executor_.Execute( + "read-gatt", + [this, request = std::move(request)]() + ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_) mutable { + absl::Time start_time = SystemClock::ElapsedRealtime(); + if (stopped_) return; + if (!status_.ok()) { + std::move(request.callback)(status_); + return; + } + SystemClock().Sleep(request.back_off.NextBackOff()); + const GattCharacteristic* characteristic = + GetCharacteristic(request.uuid_pair_index); + absl::optional value; + if (characteristic != nullptr) { + value = gatt_client_->ReadCharacteristic(*characteristic); + } + + if (stopped_) return; + if (value.has_value()) { + std::move(request.callback)(value.value()); + } else { + request.time_left -= SystemClock::ElapsedRealtime() - start_time; + if (request.time_left > absl::ZeroDuration()) { + Read(std::move(request)); + } else { + std::move(request.callback)( + absl::UnavailableError("gatt read failed")); + } + } + }); +} + +void RobustGattClient::Cleanup() { + if (gatt_client_ != nullptr && gatt_client_->IsValid()) { + gatt_client_->Disconnect(); + } + gatt_client_.reset(); + characteristics_.clear(); + MutexLock lock(&mutex_); + notify_callbacks_.clear(); +} + +void RobustGattClient::NotifyClient(absl::Status status) { + if (stopped_) return; + if (connection_status_callback_ != nullptr) + connection_status_callback_(status); +} + +} // namespace fastpair +} // namespace nearby diff --git a/fastpair/internal/mediums/robust_gatt_client.h b/fastpair/internal/mediums/robust_gatt_client.h new file mode 100644 index 00000000..f33944f3 --- /dev/null +++ b/fastpair/internal/mediums/robust_gatt_client.h @@ -0,0 +1,272 @@ +// Copyright 2023 Google LLC +// +// 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. + +#ifndef THIRD_PARTY_NEARBY_FASTPAIR_INTERNAL_ROBUST_GATT_CLIENT_H_ +#define THIRD_PARTY_NEARBY_FASTPAIR_INTERNAL_ROBUST_GATT_CLIENT_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/status/status.h" +#include "absl/time/time.h" +#include "internal/platform/ble_v2.h" +#include "internal/platform/implementation/ble_v2.h" +#include "internal/platform/single_thread_executor.h" +namespace nearby { +namespace fastpair { + +// Gatt client on top of nearby::GattClient with higher level API and built-in +// retry mechanism. All methods are not blocking unless stated otherwise. +// +// Callbacks and timeouts. +// The blocking platform calls are running on a dedicated thread. The callbacks +// are called when the platform calls have completed. The +// timeouts define for how long we will retry the platform calls before giving +// up. +// Example 1: +// `connect_timeout` is 10 seconds, `BleV2Medium::ConnectToGattServer()` fails +// after 15 seconds. In this case, we will not retry connecting, the client +// ConnectionStatusCallback will be called after 15 seconds. +// Example 2: +// `connect_timeout` is 10 seconds, `BleV2Medium::ConnectToGattServer()` fails +// after 6 seconds each time. In this case, we will retry once, the client +// ConnectionStatusCallback will be called after 12 seconds (6 + 6). +// Example 3: +// `BleV2Medium::ConnectToGattServer()` never returns. The callback will not be +// called. +class RobustGattClient { + public: + using WriteCallback = absl::AnyInvocable; + using ReadCallback = + absl::AnyInvocable value) &&>; + using NotifyCallback = + absl::AnyInvocable value)>; + using ConnectionStatusCallback = absl::AnyInvocable; + // Defines a characteristic on the server. + struct UuidPair { + // Preferred UUID, for example Fast Pair V1. + Uuid primary_uuid; + // Optional fallback UUID if the primary is not present on the server, for + // example Fast Pair V0. + Uuid fallback_uuid; + }; + struct ConnectionParams { + api::ble_v2::TxPowerLevel tx_power_level = + api::ble_v2::TxPowerLevel::kUnknown; + Uuid service_uuid; + std::vector characteristic_uuids; + // Timeout for retrying connection attempts. + absl::Duration connect_timeout = absl::Seconds(10); + // Timeout for retrying service discovery. + absl::Duration discovery_timeout = absl::Seconds(10); + // Timeout for retrying read/write/subscribe operations. + // Note, this timeout does not include the time needed to connect to the + // gatt server and discover services. + absl::Duration gatt_operation_timeout = absl::Seconds(15); + // Exponential back off parameters. They describe how long we will wait + // before retrying an operation. + absl::Duration initial_back_off_step = absl::Milliseconds(100); + absl::Duration max_back_off = absl::Seconds(3); + float back_off_multiplier = 1.5; + }; + + // Creates the GATT client, connects to the server, and discovers + // characteristics. The connection is established in the background. If the + // connection is interrupted, we will try to reconnect. The caller does not + // need to wait for connection before using the client. Instead, the caller + // should create an instance of `RobustGattClient` and immediately start + // calling other methods such as `CallRemoteFunction()`. + RobustGattClient(BleV2Medium& medium, BleV2Peripheral peripheral, + const ConnectionParams& params, + ConnectionStatusCallback callback = nullptr) + : medium_(medium), + peripheral_(peripheral), + params_(params), + connection_status_callback_(std::move(callback)) { + DCHECK_GT(params.initial_back_off_step, absl::ZeroDuration()); + DCHECK_GE(params.back_off_multiplier, 1.0); + DCHECK_GE(params.max_back_off, params.initial_back_off_step); + + Connect(); + } + + // The destructor will block if there are any ongoing platform BLE blocking + // calls. + ~RobustGattClient(); + + // Writes to the remote characteristic. + // + // `uuid_pair_index` is the index to `ConnectionParams::characteristic_uuids`. + // `callback` is called asynchronously with the result of the write call. If + // write fails with a status other than `absl::StatusCode::kUnavailable`, then + // it is a permanent failure. All future calls will likely fail too. + void WriteCharacteristic(int uuid_pair_index, absl::string_view value, + api::ble_v2::GattClient::WriteType write_type, + WriteCallback callback); + + // Performs write-and-response exchange. + // + // The call: + // * subscribes to the characteristic notifications, + // * writes to the remote characteristic, + // * waits for the response via a gatt notify call, + // * unsubsribes from the notification. + // + // `uuid_pair_index` is the index to `ConnectionParams::characteristic_uuids`. + // `response` is called asynchronously with the remote server response. If + // write fails with a status other than `absl::StatusCode::kUnavailable` or + // `absl::StatusCode::kDeadlineExceeded`, then it is a permanent failure. All + // future calls will likely fail too. + // The timeouts accumulate. If the gatt client is still connecting to the + // remote service, the call with wait for connection, discovery, + // characteristic subscription, write operation and gatt notification. Each of + // them have their own timeouts. Example: if connection takes 2 seconds, + // discovery takes 5 seconds, subscribing to the characteristic takes 3, + // writing to the characteristic takes 6 seconds but the provider never sends + // a response, then the call will time out no sooner than 2s + 5s + 3s + 6s + + // gatt_operation_timeout. + void CallRemoteFunction(int uuid_pair_index, absl::string_view request, + NotifyCallback response); + + // Reads remote characteristic. + // + // `uuid_pair_index` is the index to `ConnectionParams::characteristic_uuids`. + void ReadCharacteristic(int uuid_pair_index, ReadCallback callback); + + // Subscribes for remote characteristic updates. + // + // `uuid_pair_index` is the index to `ConnectionParams::characteristic_uuids`. + // If `call_once` is true, then the callback will be automatically + // unsubscribed after being called. + // + // Do not call `Subscribe()` or `Unsubscribe()` from the `callback`. It will + // lock up. + // Do not use `Subscribe()` and `CallRemoteFunction()` calls for the same + // characteristic at the same time. + void Subscribe(int uuid_pair_index, NotifyCallback callback, + bool call_once = false); + + // Unsubscribes from remote characteristic updates. + // + // `uuid_pair_index` is the index to `ConnectionParams::characteristic_uuids`. + void Unsubscribe(int uuid_pair_index); + + // Disables the gatt client and disconnects from the gatt server if + // connected. None of the callbacks will be triggered after `Stop()`, but if + // a callback is currently running, it may continue running after `Stop()` has + // returned. + void Stop(); + + private: + class ExpBackOff { + public: + explicit ExpBackOff(const ConnectionParams& params) + : back_off_step_(params.initial_back_off_step), + multiplier_(params.back_off_multiplier), + max_back_off_(params.max_back_off) {} + absl::Duration NextBackOff() { + absl::Duration result = back_off_; + back_off_ += std::min(back_off_ + back_off_step_, max_back_off_); + back_off_step_ *= multiplier_; + return result; + } + + private: + absl::Duration back_off_step_; + float multiplier_; + absl::Duration max_back_off_; + absl::Duration back_off_ = absl::ZeroDuration(); + }; + struct WriteRequest { + int uuid_pair_index; + std::string value; + api::ble_v2::GattClient::WriteType write_type; + WriteCallback callback; + absl::Duration time_left; + ExpBackOff back_off; + }; + struct SubscribeRequest { + int uuid_pair_index; + absl::Duration time_left; + ExpBackOff back_off; + }; + struct ReadRequest { + int uuid_pair_index; + ReadCallback callback; + absl::Duration time_left; + ExpBackOff back_off; + }; + + using GattCharacteristic = api::ble_v2::GattCharacteristic; + void Connect(); + absl::Status TryConnect() ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_); + absl::Status TryDiscoverServices() ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_); + std::vector GetPrimaryCharacteristicList(); + std::vector GetFallbackCharacteristicList(); + bool DiscoverServices(const Uuid& service_uuid, + const std::vector& characteristic_uuids) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_); + std::optional GetCharacteristic( + const Uuid& service_id, const UuidPair& characteristic_uuid_pair) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_); + // May return nullptr. + const GattCharacteristic* GetCharacteristic(int uuid_pair_index) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_); + void NotifySubscriber(int uuid_pair_index, + absl::StatusOr value); + void Cleanup() ABSL_EXCLUSIVE_LOCKS_REQUIRED(executor_); + void Write(WriteRequest request); + void Read(ReadRequest request); + void Subscribe(SubscribeRequest request); + void UnsubscribeInternal(int uuid_pair_index); + bool HasSubsriberCallback(int uuid_pair_index); + void NotifyClient(absl::Status status); + void StartNotifyTimer(int uuid_pair_index); + + // A thread for running blocking tasks. + SingleThreadExecutor executor_; + Mutex mutex_; + BleV2Medium& medium_; + BleV2Peripheral peripheral_; + ConnectionParams params_; + ConnectionStatusCallback connection_status_callback_; + std::unique_ptr gatt_client_ ABSL_GUARDED_BY(executor_); + // Mapping from `uuid_pair_index` to subscription callbacks. + // The entries are lazily initialized. + absl::flat_hash_map characteristics_ + ABSL_GUARDED_BY(executor_); + // Mapping from `uuid_pair_index` to subscription callbacks. + struct NotifyCallbackInfo { + NotifyCallback callback; + // If `call_once` is true, then the callback will be called only once, and + // then automatically unregistered. + bool call_once; + std::unique_ptr timer; + }; + absl::flat_hash_map notify_callbacks_ + ABSL_GUARDED_BY(mutex_); + std::atomic_bool stopped_ = false; + absl::Status status_ = absl::OkStatus(); +}; + +} // namespace fastpair +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_FASTPAIR_INTERNAL_ROBUST_GATT_CLIENT_H_ diff --git a/fastpair/internal/mediums/robust_gatt_client_test.cc b/fastpair/internal/mediums/robust_gatt_client_test.cc new file mode 100644 index 00000000..37277de0 --- /dev/null +++ b/fastpair/internal/mediums/robust_gatt_client_test.cc @@ -0,0 +1,612 @@ +// Copyright 2023 Google LLC +// +// 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 "fastpair/internal/mediums/robust_gatt_client.h" + +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/status/status.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "internal/platform/ble_v2.h" +#include "internal/platform/bluetooth_adapter.h" +#include "internal/platform/count_down_latch.h" +#include "internal/platform/medium_environment.h" + +namespace nearby { +namespace fastpair { +namespace { + +using ::testing::status::StatusIs; +using Property = nearby::api::ble_v2::GattCharacteristic::Property; +using Permission = nearby::api::ble_v2::GattCharacteristic::Permission; +using GattCharacteristic = nearby::api::ble_v2::GattCharacteristic; + +// Short timeout for operation that we expect to timeout. +constexpr absl::Duration kFailureTimeout = absl::Milliseconds(100); +constexpr absl::string_view kModelId = "123456"; +constexpr Uuid kFastPairServiceUuid(0x0000FE2C00001000, 0x800000805F9B34FB); +constexpr Uuid kKeyBasedCharacteristicUuidV1(0x0000123400001000, + 0x800000805F9B34FB); +constexpr Uuid kKeyBasedCharacteristicUuidV2(0xFE2C123483664814, + 0x8EB001DE32100BEA); +constexpr Uuid kPasskeyCharacteristicUuidV1(0x0000123500001000, + 0x800000805F9B34FB); +constexpr Uuid kPasskeyCharacteristicUuidV2(0xFE2C123583664814, + 0x8EB001DE32100BEA); +constexpr Uuid kAccountKeyCharacteristicUuidV1(0x0000123600001000, + 0x800000805F9B34FB); +constexpr Uuid kAccountKeyCharacteristicUuidV2(0xFE2C123683664814, + 0x8EB001DE32100BEA); + +constexpr Uuid kModelIdCharacteristics(0xFE2C123383664814, 0x8EB001DE32100BEA); +class MediumEnvironmentStarter { + public: + MediumEnvironmentStarter() { MediumEnvironment::Instance().Start({}); } + ~MediumEnvironmentStarter() { MediumEnvironment::Instance().Stop(); } +}; + +struct CharacteristicData { + // Write result returned to the gatt client. + absl::Status write_result = + absl::PermissionDeniedError("can't write characteristic"); + std::string written_data; + absl::StatusOr read_result = + absl::PermissionDeniedError("can't read characteristic"); + std::optional notify_response; +}; + +class RobustGattClientTest : public testing::Test { + protected: + void SetUp() override { StartGattServer(); } + + void StartGattServer() { + gatt_server_ = + provider_ble_.StartGattServer(/*ServerGattConnectionCallback=*/{ + .on_characteristic_read_cb = + [&](const api::ble_v2::BlePeripheral& remote_device, + const api::ble_v2::GattCharacteristic& characteristic, + int offset, + BleV2Medium::ServerGattConnectionCallback::ReadValueCallback + callback) { + auto it = characteristics_.find(characteristic); + if (it == characteristics_.end()) { + callback(absl::NotFoundError("characteristic not found")); + return; + } + callback(it->second.read_result); + }, + .on_characteristic_write_cb = + [&](const api::ble_v2::BlePeripheral& remote_device, + const api::ble_v2::GattCharacteristic& characteristic, + int offset, absl::string_view data, + BleV2Medium::ServerGattConnectionCallback:: + WriteValueCallback callback) { + auto it = characteristics_.find(characteristic); + if (it == characteristics_.end()) { + callback(absl::NotFoundError("characteristic not found")); + return; + } + it->second.written_data = data; + if (it->second.write_result.code() == + absl::StatusCode::kDeadlineExceeded) { + absl::SleepFor(kFailureTimeout); + } + callback(it->second.write_result); + if (it->second.notify_response.has_value()) { + auto ignored = gatt_server_->NotifyCharacteristicChanged( + characteristic, false, + ByteArray(*it->second.notify_response)); + } + }, + }); + provider_address_ = *gatt_server_->GetBlePeripheral().GetAddress(); + } + + void InsertCorrectV2GattCharacteristics() { + key_based_characteristic_ = gatt_server_->CreateCharacteristic( + kFastPairServiceUuid, kKeyBasedCharacteristicUuidV2, permissions_, + properties_); + characteristics_[*key_based_characteristic_].write_result = + absl::OkStatus(); + + passkey_characteristic_ = gatt_server_->CreateCharacteristic( + kFastPairServiceUuid, kPasskeyCharacteristicUuidV2, permissions_, + properties_); + characteristics_[*passkey_characteristic_].write_result = absl::OkStatus(); + + accountkey_characteristic_ = gatt_server_->CreateCharacteristic( + kFastPairServiceUuid, kAccountKeyCharacteristicUuidV2, permissions_, + properties_); + characteristics_[*accountkey_characteristic_].write_result = + absl::OkStatus(); + model_id_characteristic_ = gatt_server_->CreateCharacteristic( + kFastPairServiceUuid, kModelIdCharacteristics, Permission::kRead, + Property::kRead); + characteristics_[*model_id_characteristic_].read_result = kModelId; + } + + void InsertCorrectV1GattCharacteristics() { + key_based_characteristic_ = gatt_server_->CreateCharacteristic( + kFastPairServiceUuid, kKeyBasedCharacteristicUuidV1, permissions_, + properties_); + characteristics_[*key_based_characteristic_].write_result = + absl::OkStatus(); + + passkey_characteristic_ = gatt_server_->CreateCharacteristic( + kFastPairServiceUuid, kPasskeyCharacteristicUuidV1, permissions_, + properties_); + characteristics_[*passkey_characteristic_].write_result = absl::OkStatus(); + + accountkey_characteristic_ = gatt_server_->CreateCharacteristic( + kFastPairServiceUuid, kAccountKeyCharacteristicUuidV1, permissions_, + properties_); + characteristics_[*accountkey_characteristic_].write_result = + absl::OkStatus(); + } + + MediumEnvironmentStarter env_; + BluetoothAdapter provider_adapter_; + BleV2Medium provider_ble_{provider_adapter_}; + BluetoothAdapter seeker_adapter_; + BleV2Medium seeker_ble_{seeker_adapter_}; + std::unique_ptr gatt_server_; + std::string provider_address_; + absl::flat_hash_map characteristics_; + Property properties_ = Property::kWrite | Property::kNotify; + Permission permissions_ = Permission::kWrite; + std::optional key_based_characteristic_; + std::optional passkey_characteristic_; + std::optional accountkey_characteristic_; + std::optional model_id_characteristic_; +}; + +TEST_F(RobustGattClientTest, Constructor) { + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + RobustGattClient gatt_client(seeker_ble_, provider, {}); +} + +TEST_F(RobustGattClientTest, ConnectToProviderWithoutServiceFails) { + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + params.connect_timeout = absl::Seconds(1); + params.discovery_timeout = absl::Seconds(1); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.WriteCharacteristic( + 0, "hello", api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + EXPECT_THAT(status, StatusIs(absl::StatusCode::kDeadlineExceeded)); + latch.CountDown(); + }); + EXPECT_TRUE(latch.Await().Ok()); +} + +TEST_F(RobustGattClientTest, DiscoveryRetryWorks) { + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.WriteCharacteristic( + 0, "hello", api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + EXPECT_OK(status); + latch.CountDown(); + }); + // The Gatt client is re-trying to discover the characteristics. + InsertCorrectV2GattCharacteristics(); + + EXPECT_TRUE(latch.Await().Ok()); +} + +TEST_F(RobustGattClientTest, SuccessfulWriteToPrimaryUuid) { + constexpr absl::string_view kData = "hello"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.WriteCharacteristic( + 0, kData, api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + EXPECT_OK(status); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); + EXPECT_EQ(characteristics_[*key_based_characteristic_].written_data, kData); +} + +TEST_F(RobustGattClientTest, SuccessfulWriteToFallbackUuid) { + constexpr absl::string_view kData = "hello"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV1GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.WriteCharacteristic( + 0, kData, api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + EXPECT_OK(status); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); + EXPECT_EQ(characteristics_[*key_based_characteristic_].written_data, kData); +} + +TEST_F(RobustGattClientTest, RejectedWrite) { + constexpr absl::string_view kData = "hello"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + characteristics_[*key_based_characteristic_].write_result = + absl::UnauthenticatedError("write rejected"); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + params.gatt_operation_timeout = kFailureTimeout; + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.WriteCharacteristic( + 0, kData, api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + EXPECT_THAT(status, StatusIs(absl::StatusCode::kUnavailable)); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); +} + +TEST_F(RobustGattClientTest, SuccessfulCallRemoteFunctionToPrimaryUuid) { + constexpr absl::string_view kRequest = "request"; + constexpr absl::string_view kResponse = "response"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + characteristics_[*key_based_characteristic_].notify_response = kResponse; + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.CallRemoteFunction( + 0, kRequest, [&](absl::StatusOr response) { + EXPECT_OK(response); + EXPECT_EQ(*response, kResponse); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); + EXPECT_EQ(characteristics_[*key_based_characteristic_].written_data, + kRequest); +} + +TEST_F(RobustGattClientTest, SuccessfulCallRemoteFunctionToFallbackUuid) { + constexpr absl::string_view kRequest = "request"; + constexpr absl::string_view kResponse = "response"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV1GattCharacteristics(); + characteristics_[*key_based_characteristic_].notify_response = kResponse; + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.CallRemoteFunction( + 0, kRequest, [&](absl::StatusOr response) { + EXPECT_OK(response); + EXPECT_EQ(*response, kResponse); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); + EXPECT_EQ(characteristics_[*key_based_characteristic_].written_data, + kRequest); +} + +TEST_F(RobustGattClientTest, CallRemoteFunctionRejectedWrite) { + constexpr absl::string_view kRequest = "request"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + characteristics_[*key_based_characteristic_].write_result = + absl::InternalError("write rejected"); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + params.gatt_operation_timeout = kFailureTimeout; + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.CallRemoteFunction( + 0, kRequest, [&](absl::StatusOr response) { + EXPECT_THAT(response.status(), + StatusIs(absl::StatusCode::kUnavailable)); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); +} + +TEST_F(RobustGattClientTest, CallRemoteFunctionNoResponseTimesOut) { + constexpr absl::string_view kRequest = "request"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + params.gatt_operation_timeout = kFailureTimeout; + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.CallRemoteFunction( + 0, kRequest, [&](absl::StatusOr response) { + EXPECT_THAT(response.status(), + StatusIs(absl::StatusCode::kDeadlineExceeded)); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); +} + +TEST_F(RobustGattClientTest, WriteTimeout) { + constexpr absl::string_view kData = "hello"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + characteristics_[*key_based_characteristic_].write_result = + absl::DeadlineExceededError("write time out"); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + params.gatt_operation_timeout = kFailureTimeout; + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.WriteCharacteristic( + 0, kData, api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + EXPECT_THAT(status, StatusIs(absl::StatusCode::kUnavailable)); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); +} + +TEST_F(RobustGattClientTest, ReconnectWorks) { + constexpr absl::string_view kData = "hello"; + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.WriteCharacteristic( + 0, "first", api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + NEARBY_LOGS(INFO) << "First write completed with: " << status; + EXPECT_OK(status); + latch.CountDown(); + }); + EXPECT_TRUE(latch.Await().Ok()); + CountDownLatch write_after_reconnect(1); + gatt_client.WriteCharacteristic( + 0, kData, api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { + NEARBY_LOGS(INFO) << "Write after reconnect completed with: " << status; + EXPECT_OK(status); + write_after_reconnect.CountDown(); + }); + gatt_server_->Stop(); + gatt_server_.reset(); + StartGattServer(); + InsertCorrectV2GattCharacteristics(); + EXPECT_TRUE(write_after_reconnect.Await().Ok()); + EXPECT_EQ(characteristics_[*key_based_characteristic_].written_data, kData); +} + +TEST_F(RobustGattClientTest, SuccessfulSubscribeToPrimaryUuid) { + constexpr absl::string_view kData = "hello"; + constexpr int kKeyBasedCharacteristicIndex = 0; + CountDownLatch write_latch(1); + CountDownLatch notify_latch(1); + absl::StatusOr notify_data; + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.Subscribe(kKeyBasedCharacteristicIndex, + [&](absl::StatusOr data) { + notify_data = data; + notify_latch.CountDown(); + }); + gatt_client.WriteCharacteristic( + 0, "", api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { write_latch.CountDown(); }); + EXPECT_TRUE(write_latch.Await().Ok()); + EXPECT_OK(gatt_server_->NotifyCharacteristicChanged( + *key_based_characteristic_, + /*confirm=*/true, ByteArray(std::string(kData)))); + + EXPECT_TRUE(notify_latch.Await().Ok()); + EXPECT_OK(notify_data); + EXPECT_EQ(*notify_data, kData); +} + +TEST_F(RobustGattClientTest, SuccessfulSubscribeToFallbackUuid) { + constexpr absl::string_view kData = "hello"; + constexpr int kKeyBasedCharacteristicIndex = 0; + CountDownLatch write_latch(1); + CountDownLatch notify_latch(1); + absl::StatusOr notify_data; + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV1GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.Subscribe(kKeyBasedCharacteristicIndex, + [&](absl::StatusOr data) { + notify_data = data; + notify_latch.CountDown(); + }); + gatt_client.WriteCharacteristic( + 0, "", api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { write_latch.CountDown(); }); + EXPECT_TRUE(write_latch.Await().Ok()); + EXPECT_OK(gatt_server_->NotifyCharacteristicChanged( + *key_based_characteristic_, + /*confirm=*/true, ByteArray(std::string(kData)))); + + EXPECT_TRUE(notify_latch.Await().Ok()); + EXPECT_OK(notify_data); + EXPECT_EQ(*notify_data, kData); +} + +TEST_F(RobustGattClientTest, NoNotifyAfterUnsubscribe) { + constexpr absl::string_view kData = "hello"; + constexpr int kKeyBasedCharacteristicIndex = 0; + CountDownLatch write_latch(1); + CountDownLatch notify_latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.Subscribe(kKeyBasedCharacteristicIndex, + [&](absl::StatusOr data) { + NEARBY_LOGS(INFO) + << "Notified " << data.status() << ", " << *data; + notify_latch.CountDown(); + }); + gatt_client.WriteCharacteristic( + 0, "", api::ble_v2::GattClient::WriteType::kWithResponse, + [&](absl::Status status) { write_latch.CountDown(); }); + EXPECT_TRUE(write_latch.Await().Ok()); + + gatt_client.Unsubscribe(kKeyBasedCharacteristicIndex); + // Depending on the timing, the characteristic may still be subscribed on the + // server. `NotifyCharacteristicChanged()` may be successful or may fail. + // Neither is an error. + auto ignored = gatt_server_->NotifyCharacteristicChanged( + *key_based_characteristic_, + /*confirm=*/true, ByteArray(std::string(kData))); + + EXPECT_FALSE(notify_latch.Await(kFailureTimeout).result()); +} + +TEST_F(RobustGattClientTest, SuccessfulRead) { + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + params.characteristic_uuids.push_back({kModelIdCharacteristics, Uuid()}); + constexpr int kModelIdIndex = 1; + + RobustGattClient gatt_client(seeker_ble_, provider, params); + gatt_client.ReadCharacteristic(kModelIdIndex, + [&](absl::StatusOr value) { + EXPECT_OK(value); + EXPECT_EQ(*value, kModelId); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); +} + +TEST_F(RobustGattClientTest, ReadFromWrongCharacteristicFails) { + CountDownLatch latch(1); + BleV2Peripheral provider = seeker_ble_.GetRemotePeripheral(provider_address_); + InsertCorrectV2GattCharacteristics(); + RobustGattClient::ConnectionParams params; + params.tx_power_level = api::ble_v2::TxPowerLevel::kMedium; + params.service_uuid = kFastPairServiceUuid; + params.characteristic_uuids.push_back( + {kKeyBasedCharacteristicUuidV2, kKeyBasedCharacteristicUuidV1}); + params.characteristic_uuids.push_back({kModelIdCharacteristics, Uuid()}); + params.gatt_operation_timeout = kFailureTimeout; + constexpr int kKeyBasedCharacteristicIndex = 0; + + RobustGattClient gatt_client(seeker_ble_, provider, params); + // Key based pairing characteristic is write only, so read will fail. + gatt_client.ReadCharacteristic( + kKeyBasedCharacteristicIndex, + [&](absl::StatusOr value) { + EXPECT_THAT(value.status(), StatusIs(absl::StatusCode::kUnavailable)); + latch.CountDown(); + }); + + EXPECT_TRUE(latch.Await().Ok()); +} + +} // namespace + +} // namespace fastpair +} // namespace nearby diff --git a/internal/platform/implementation/BUILD b/internal/platform/implementation/BUILD index 2ddde5dd..d2d9b6be 100644 --- a/internal/platform/implementation/BUILD +++ b/internal/platform/implementation/BUILD @@ -83,6 +83,8 @@ cc_library( copts = ["-DNO_WEBRTC"], visibility = [ "//connections/implementation:__subpackages__", + "//fastpair/internal:__pkg__", + "//fastpair/internal/mediums:__pkg__", "//internal/network:__subpackages__", "//internal/platform:__pkg__", "//internal/platform/implementation:__subpackages__", diff --git a/internal/platform/implementation/g3/ble_v2.cc b/internal/platform/implementation/g3/ble_v2.cc index cbd5f4e7..5354b937 100644 --- a/internal/platform/implementation/g3/ble_v2.cc +++ b/internal/platform/implementation/g3/ble_v2.cc @@ -581,6 +581,7 @@ bool BleV2Medium::GattServer::HasCharacteristic( } void BleV2Medium::GattServer::Stop() { + if (stopped_) return; NEARBY_LOGS(INFO) << "G3 Ble GattServer Stop"; stopped_ = true; characteristics_.clear();