Files
nearby/internal/platform/implementation/linux/ble_gatt_client.cc
T

464 lines
17 KiB
C++

// 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 <algorithm>
#include <map>
#include <string>
#include <variant>
#include <sdbus-c++/Types.h>
#include "absl/strings/substitute.h"
#include "absl/synchronization/mutex.h"
#include "internal/platform/cancellation_flag_listener.h"
#include "internal/platform/implementation/ble.h"
#include "internal/platform/implementation/linux/ble_gatt_client.h"
#include "internal/platform/implementation/linux/bluez_gatt_characteristic_client.h"
#include "internal/platform/implementation/linux/bluez_gatt_service_client.h"
#include "internal/platform/implementation/linux/dbus.h"
#include "internal/platform/implementation/linux/generated/dbus/bluez/gatt_characteristic_client.h"
#include "internal/platform/implementation/linux/generated/dbus/bluez/gatt_service_client.h"
#include "internal/platform/implementation/linux/utils.h"
#include "internal/platform/logging.h"
namespace nearby {
namespace linux {
bool GattClient::DiscoverServiceAndCharacteristics(
const Uuid &service_uuid, const std::vector<Uuid> &characteristic_uuids) {
LOG(INFO) << __func__ << ": Discovering service " << std::string(service_uuid)
<< " with " << characteristic_uuids.size()
<< " characteristic UUID(s) on peripheral "
<< peripheral_object_path_;
bool success = gatt_discovery_->DiscoverServiceAndCharacteristics(
peripheral_object_path_, service_uuid, characteristic_uuids,
discovery_cancel_);
LOG(INFO) << __func__ << ": Discovery "
<< (success ? "succeeded" : "failed") << " for service "
<< std::string(service_uuid) << " on peripheral "
<< peripheral_object_path_;
return success;
}
absl::optional<api::ble::GattCharacteristic> GattClient::GetCharacteristic(
const Uuid &service_uuid, const Uuid &characteristic_uuid) {
LOG(INFO) << __func__ << ": Looking up characteristic "
<< std::string(characteristic_uuid) << " in service "
<< std::string(service_uuid) << " on peripheral "
<< peripheral_object_path_;
auto chr_proxy = gatt_discovery_->GetCharacteristic(
peripheral_object_path_, service_uuid, characteristic_uuid);
if (chr_proxy == nullptr) {
LOG(INFO) << __func__ << ": Characteristic lookup failed for service "
<< std::string(service_uuid) << ", characteristic "
<< std::string(characteristic_uuid);
return std::nullopt;
}
api::ble::GattCharacteristic chr;
chr.service_uuid = service_uuid;
chr.uuid = characteristic_uuid;
chr.property = api::ble::GattCharacteristic::Property::kNone;
chr.permission = api::ble::GattCharacteristic::Permission::kNone;
std::vector<std::string> flags;
try {
flags = chr_proxy->Flags();
} catch (const sdbus::Error &e) {
DBUS_LOG_PROPERTY_GET_ERROR(chr_proxy, "Flags", e);
return std::nullopt;
}
for (const auto &flag : flags) {
if (flag == "read") {
chr.property |= api::ble::GattCharacteristic::Property::kRead;
chr.permission |= api::ble::GattCharacteristic::Permission::kRead;
} else if (flag == "write") {
chr.property |= api::ble::GattCharacteristic::Property::kWrite;
chr.permission |= api::ble::GattCharacteristic::Permission::kWrite;
} else if (flag == "notify") {
chr.property |= api::ble::GattCharacteristic::Property::kNotify;
} else if (flag == "indicate") {
chr.property |= api::ble::GattCharacteristic::Property::kIndicate;
}
}
absl::MutexLock lock(&characteristics_mutex_);
characteristics_.emplace(chr, std::move(chr_proxy));
LOG(INFO) << __func__ << ": Cached characteristic for service "
<< std::string(service_uuid) << ", characteristic "
<< std::string(characteristic_uuid);
return chr;
}
absl::optional<std::string> GattClient::ReadCharacteristic(
const api::ble::GattCharacteristic &characteristic) {
LOG(INFO) << __func__ << ": Reading characteristic '"
<< absl::Substitute("$0", characteristic) << "'";
absl::ReaderMutexLock lock(&characteristics_mutex_);
if (characteristics_.count(characteristic) == 0) {
LOG(ERROR) << __func__ << ": Unknown characteristic '"
<< absl::Substitute("$0", characteristic) << "'";
return std::nullopt;
}
auto result = std::visit(
[](auto &&chr) {
try {
auto value_bytes = chr->ReadValue({});
return std::optional<std::string>{
std::string(value_bytes.begin(), value_bytes.end())};
} catch (const sdbus::Error &e) {
DBUS_LOG_METHOD_CALL_ERROR(chr, "ReadValue", e);
return std::optional<std::string>();
}
},
characteristics_[characteristic]);
LOG(INFO) << __func__ << ": Read "
<< (result.has_value() ? "succeeded" : "failed")
<< " for characteristic '"
<< absl::Substitute("$0", characteristic) << "'";
return result;
}
bool GattClient::WriteCharacteristic(
const api::ble::GattCharacteristic &characteristic,
absl::string_view value, WriteType type) {
LOG(INFO) << __func__ << ": Writing " << value.size()
<< " byte(s) to characteristic '"
<< absl::Substitute("$0", characteristic) << "' with write type "
<< (type == api::ble::GattClient::WriteType::kWithResponse
? "with_response"
: "without_response");
absl::ReaderMutexLock lock(&characteristics_mutex_);
if (characteristics_.count(characteristic) == 0) {
LOG(ERROR) << __func__ << ": Unknown characteristic '"
<< absl::Substitute("$0", characteristic) << "'";
return false;
}
bool success = std::visit(
[value, type](auto &&chr) {
std::vector<uint8_t> value_bytes(value.begin(), value.end());
try {
chr->WriteValue(
value_bytes,
{{sdbus::PropertyName("type"),
sdbus::Variant(type == api::ble::GattClient::WriteType::kWithResponse
? "request"
: "command")}});
return true;
} catch (const sdbus::Error &e) {
DBUS_LOG_METHOD_CALL_ERROR(chr, "WriteValue", e);
return false;
}
},
characteristics_[characteristic]);
LOG(INFO) << __func__ << ": Write "
<< (success ? "succeeded" : "failed")
<< " for characteristic '" << absl::Substitute("$0", characteristic)
<< "'";
return success;
}
void GattClient::Disconnect() {
LOG(INFO) << __func__
<< ": Disconnecting GATT client for peripheral "
<< peripheral_object_path_;
absl::MutexLock lock(&disconnected_callback_mutex_);
if (!discovery_cancel_.Cancelled()) {
discovery_cancel_.Cancel();
if (*disconnected_callback_it_ != nullptr) (*disconnected_callback_it_)();
gatt_discovery_->RemovePeripheralConnection(peripheral_object_path_,
disconnected_callback_it_);
LOG(INFO) << __func__
<< ": Disconnected GATT client for peripheral "
<< peripheral_object_path_;
return;
}
LOG(INFO) << __func__
<< ": GATT client already disconnected for peripheral "
<< peripheral_object_path_;
}
void BluezGattDiscovery::Shutdown() {
auto no_discovery = [&]() {
mutex_.AssertReaderHeld();
return pending_discovery_ == 0;
};
mutex_.Lock();
shutdown_ = true;
mutex_.Await(absl::Condition(&no_discovery));
mutex_.Unlock();
}
bool BluezGattDiscovery::InitializeKnownServices() {
std::map<sdbus::ObjectPath,
std::map<sdbus::InterfaceName, std::map<sdbus::PropertyName, sdbus::Variant>>>
objects;
try {
objects = GetManagedObjects();
} catch (const sdbus::Error &e) {
DBUS_LOG_METHOD_CALL_ERROR(this, "GetManagedObjects", e);
return false;
}
absl::MutexLock lock(&mutex_);
auto chr_it = std::find_if(
objects.cbegin(), objects.cend(),
[](std::pair<sdbus::ObjectPath,
std::map<sdbus::InterfaceName, std::map<sdbus::PropertyName, sdbus::Variant>>>
object) {
return object.second.count(
sdbus::InterfaceName(org::bluez::GattCharacteristic1_proxy::INTERFACE_NAME)) == 1;
});
for (; chr_it != objects.cend(); ++chr_it) {
const auto& [path, ifaces] = *chr_it;
auto iface_it = ifaces.find(sdbus::InterfaceName(
org::bluez::GattCharacteristic1_proxy::INTERFACE_NAME));
if (iface_it == ifaces.end()) {
continue;
}
const auto& properties = iface_it->second;
auto maybe_props = characteristicProperties(path, properties);
if (!maybe_props.has_value()) continue;
auto [service_uuid, chr_uuid, device_path] = *maybe_props;
discovered_characteristics_.emplace(
std::make_tuple(service_uuid, chr_uuid, device_path), path);
characteristics_properties_.emplace(
path, std::make_tuple(service_uuid, chr_uuid, device_path));
}
return true;
}
BluezGattDiscovery::CallbackIter BluezGattDiscovery::AddPeripheralConnection(
const sdbus::ObjectPath &device_object_path,
absl::AnyInvocable<void()> disconnected_callback_) {
absl::MutexLock lock(&peripheral_disconnected_callbacks_mutex_);
if (peripheral_disconnected_callbacks_.count(device_object_path) == 0)
peripheral_disconnected_callbacks_.emplace(
device_object_path, std::list<absl::AnyInvocable<void()>>{});
auto &list = peripheral_disconnected_callbacks_[device_object_path];
list.push_back(std::move(disconnected_callback_));
return list.begin();
}
void BluezGattDiscovery::RemovePeripheralConnection(
const sdbus::ObjectPath &device_object_path,
BluezGattDiscovery::CallbackIter cb) {
absl::MutexLock lock(&peripheral_disconnected_callbacks_mutex_);
auto it = peripheral_disconnected_callbacks_.find(device_object_path);
if (it != peripheral_disconnected_callbacks_.end()) {
it->second.erase(cb);
if (it->second.empty())
peripheral_disconnected_callbacks_.erase(device_object_path);
}
}
bool BluezGattDiscovery::DiscoverServiceAndCharacteristics(
const sdbus::ObjectPath &device_object_path, const Uuid &service_uuid,
const std::vector<Uuid> &characteristic_uuids, CancellationFlag &cancel) {
CancellationFlagListener cancel_listen(&cancel, [&]() {
mutex_.Lock();
mutex_.Unlock();
});
auto discovered = [this, device_object_path, service_uuid,
characteristic_uuids, &cancel]() {
mutex_.AssertReaderHeld();
return cancel.Cancelled() ||
std::all_of(
characteristic_uuids.cbegin(), characteristic_uuids.cend(),
[this, service_uuid, device_object_path](auto &chr_uuid) {
mutex_.AssertReaderHeld();
return discovered_characteristics_.count(
{service_uuid, chr_uuid, device_object_path}) == 1;
});
};
absl::ReaderMutexLock lock(&mutex_, absl::Condition(&discovered));
LOG(INFO) << __func__ << ": Finished discovering gatt services and characteristics";
return !cancel.Cancelled();
}
std::unique_ptr<bluez::GattCharacteristicClient>
BluezGattDiscovery::GetCharacteristic(
const sdbus::ObjectPath &device_object_path, const Uuid &service_uuid,
const Uuid &characteristic_uuid) {
auto key =
std::make_tuple(service_uuid, characteristic_uuid, device_object_path);
absl::ReaderMutexLock lock(&mutex_);
auto path_it = discovered_characteristics_.find(key);
if (path_it == discovered_characteristics_.end()) {
LOG(ERROR) << __func__ << ": No characteristic known for device "
<< device_object_path << " with service "
<< std::string{service_uuid} << " and UUID "
<< std::string{characteristic_uuid};
return nullptr;
}
return std::make_unique<bluez::GattCharacteristicClient>(system_bus_,
path_it->second);
}
std::unique_ptr<bluez::GattCharacteristicClient>
BluezGattDiscovery::GetSubscribedCharacteristic(
const sdbus::ObjectPath &device_object_path, const Uuid &service_uuid,
const Uuid &characteristic_uuid,
absl::AnyInvocable<void(absl::string_view value)>
on_characteristic_changed_cb) {
auto key =
std::make_tuple(service_uuid, characteristic_uuid, device_object_path);
absl::ReaderMutexLock lock(&mutex_);
auto path_it = discovered_characteristics_.find(key);
if (path_it == discovered_characteristics_.end()) {
LOG(ERROR) << __func__ << ": No characteristic known for device "
<< device_object_path << " with service "
<< std::string{service_uuid} << " and UUID "
<< std::string{characteristic_uuid};
return nullptr;
}
return std::make_unique<bluez::SubscribedGattCharacteristicClient>(
system_bus_, path_it->second, std::move(on_characteristic_changed_cb));
}
std::optional<std::tuple<Uuid, Uuid, sdbus::ObjectPath>>
BluezGattDiscovery::characteristicProperties(
const sdbus::ObjectPath &char_path,
const std::map<sdbus::PropertyName, sdbus::Variant> &properties) {
mutex_.AssertHeld();
const std::string &chr_uuid_str =
properties.at(sdbus::PropertyName("UUID")).get<std::string>();
auto chr_uuid = UuidFromString(chr_uuid_str);
if (!chr_uuid.has_value()) {
LOG(ERROR) << ": Couldn't parse UUID '" << chr_uuid_str
<< "' in characteristic " << char_path;
return std::nullopt;
}
const sdbus::ObjectPath &service_path =
properties.at(sdbus::PropertyName("Service")).get<sdbus::ObjectPath>();
if (cached_services_.count(service_path) == 0) {
cached_services_.emplace(
service_path, std::make_unique<GattServiceClient>(system_bus_, service_path));
}
auto it = cached_services_.find(service_path);
if (it == cached_services_.end() || it->second == nullptr) {
LOG(ERROR) << ": cached_services_ missing service " << service_path
<< " (from characteristic " << char_path << ")";
return std::nullopt;
}
LOG(INFO) << ": Found service path " << service_path
<< " (from characteristic " << char_path << ")";
auto* service = it->second.get(); // service is GattServiceClient*
nearby::Uuid service_uuid;
try {
std::string service_uuid_str = service->UUID(); // copy (safe)
auto service_uuid_maybe = UuidFromString(service_uuid_str);
if (!service_uuid_maybe.has_value()) {
LOG(ERROR) << ": Couldn't parse UUID '" << service_uuid_str
<< "' in service " << service_path;
return std::nullopt;
}
service_uuid = *service_uuid_maybe;
} catch (const sdbus::Error &e) {
DBUS_LOG_PROPERTY_GET_ERROR(service, "UUID", e);
return std::nullopt;
}
sdbus::ObjectPath device_path;
try {
device_path = service->Device();
} catch (const sdbus::Error &e) {
DBUS_LOG_PROPERTY_GET_ERROR(service, "Device", e);
return std::nullopt;
}
return std::make_tuple(service_uuid, *chr_uuid, device_path);
}
void BluezGattDiscovery::onInterfacesAdded(
const sdbus::ObjectPath &objectPath,
const std::map<sdbus::InterfaceName,
std::map<sdbus::PropertyName, sdbus::Variant>>
&interfacesAndProperties) {
if (interfacesAndProperties.count(
sdbus::InterfaceName(org::bluez::GattCharacteristic1_proxy::INTERFACE_NAME)) == 0)
return;
const auto &properties = interfacesAndProperties.at(
sdbus::InterfaceName(org::bluez::GattCharacteristic1_proxy::INTERFACE_NAME));
absl::MutexLock lock(&mutex_);
auto maybe_props = characteristicProperties(objectPath, properties);
if (!maybe_props.has_value()) return;
auto [service_uuid, chr_uuid, device_path] = *maybe_props;
discovered_characteristics_.emplace(
std::make_tuple(service_uuid, chr_uuid, device_path), objectPath);
characteristics_properties_.emplace(
objectPath, std::make_tuple(service_uuid, chr_uuid, device_path));
}
void BluezGattDiscovery::onInterfacesRemoved(
const sdbus::ObjectPath &objectPath,
const std::vector<sdbus::InterfaceName> &interfaces) {
auto begin = interfaces.cbegin();
auto end = interfaces.cend();
auto service_it =
std::find(begin, end, org::bluez::GattService1_proxy::INTERFACE_NAME);
if (service_it != end) {
absl::MutexLock lock(&mutex_);
cached_services_.erase(objectPath);
return;
}
auto chr_it = std::find(
begin, end, org::bluez::GattCharacteristic1_proxy::INTERFACE_NAME);
if (chr_it != end) {
absl::MutexLock lock(&mutex_);
{
auto it = characteristics_properties_.find(objectPath);
if (it == characteristics_properties_.end()) {
// Not tracked / already removed / never added.
// return; // or just `break;` / `continue;` depending on your context
return;
}
auto &props = it->second;
discovered_characteristics_.erase(props);
}
characteristics_properties_.erase(objectPath);
}
}
} // namespace linux
} // namespace nearby