blob: 15399b216773f06956a8144de1e6d5b5fb99adca [file]
// Copyright 2025 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_proxy/gatt/gatt.h"
#include <mutex>
#include "lib/stdcompat/utility.h"
#include "pw_assert/check.h"
#include "pw_bluetooth/att.emb.h"
#include "pw_bluetooth/emboss_util.h"
#include "pw_bluetooth/l2cap_frames.emb.h"
#include "pw_containers/algorithm.h"
#include "pw_log/log.h"
#include "pw_span/cast.h"
namespace pw::bluetooth::proxy::gatt {
void Client::Delegate::HandleNotification(ConnectionHandle connection_handle,
AttributeHandle value_handle,
multibuf::MultiBuf&& value) {
DoHandleNotification(connection_handle, value_handle, std::move(value));
}
void Client::Delegate::HandleError(Error error,
ConnectionHandle connection_handle) {
DoHandleError(error, connection_handle);
}
Client::Client()
: client_id_(internal::ClientId{0}),
connection_handle_(ConnectionHandle{0}),
gatt_(nullptr) {}
Client::~Client() { Close(); }
Client::Client(Client&& other) { Move(std::move(other)); }
Client& Client::operator=(Client&& other) {
Move(std::move(other));
return *this;
}
void Client::Move(Client&& other) {
if (gatt_ != nullptr) {
Close();
}
client_id_ = other.client_id_;
connection_handle_ = other.connection_handle_;
gatt_ = std::exchange(other.gatt_, nullptr);
}
Client::Client(internal::ClientId client_id,
ConnectionHandle connection_handle,
Gatt& gatt)
: client_id_(client_id),
connection_handle_(connection_handle),
gatt_(&gatt) {}
void Client::Close() {
if (gatt_ == nullptr) {
// Already closed
return;
}
gatt_->UnregisterClient(client_id_, connection_handle_);
gatt_ = nullptr;
}
Status Client::InterceptNotification(AttributeHandle value_handle) {
return gatt_->InterceptNotification(
client_id_, connection_handle_, value_handle);
}
Status Client::CancelInterceptNotification(AttributeHandle value_handle) {
return gatt_->CancelInterceptNotification(
client_id_, connection_handle_, value_handle);
}
void Server::Delegate::HandleWriteWithoutResponse(
ConnectionHandle connection_handle,
AttributeHandle value_handle,
multibuf::MultiBuf&& value) {
DoHandleWriteWithoutResponse(
connection_handle, value_handle, std::move(value));
}
void Server::Delegate::HandleWriteAvailable(
ConnectionHandle connection_handle) {
DoHandleWriteAvailable(connection_handle);
}
void Server::Delegate::HandleError(Error error,
ConnectionHandle connection_handle) {
DoHandleError(error, connection_handle);
}
Server::Server(internal::ServerId server_id,
ConnectionHandle connection_handle,
Gatt& gatt)
: server_id_(server_id),
connection_handle_(connection_handle),
gatt_(&gatt) {}
Server::~Server() { Close(); }
Server::Server(Server&& other) { Move(std::move(other)); }
Server& Server::operator=(Server&& other) {
Move(std::move(other));
return *this;
}
void Server::Move(Server&& other) {
if (gatt_ != nullptr) {
Close();
}
server_id_ = other.server_id_;
connection_handle_ = other.connection_handle_;
gatt_ = std::exchange(other.gatt_, nullptr);
}
void Server::Close() {
if (gatt_ == nullptr) {
// Already closed
return;
}
gatt_->UnregisterServer(server_id_, connection_handle_);
gatt_ = nullptr;
}
Status Server::AddCharacteristic(CharacteristicInfo characteristic) {
if (gatt_ == nullptr) {
return Status::FailedPrecondition();
;
}
return gatt_->AddCharacteristic(
server_id_, connection_handle_, characteristic);
}
Status Server::RemoveCharacteristic(CharacteristicInfo characteristic) {
if (gatt_ == nullptr) {
return Status::FailedPrecondition();
}
return gatt_->RemoveCharacteristic(
server_id_, connection_handle_, characteristic);
}
StatusWithMultiBuf Server::SendNotification(AttributeHandle value_handle,
multibuf::MultiBuf&& value) {
if (gatt_ == nullptr) {
return {.status = Status::FailedPrecondition(), .buf = std::move(value)};
}
return gatt_->SendNotification(
server_id_, connection_handle_, value_handle, std::move(value));
}
Gatt::Gatt(L2capChannelManagerInterface& l2cap,
Allocator& allocator,
multibuf::MultiBufAllocator& multibuf_allocator)
: l2cap_(l2cap),
allocator_(allocator),
multibuf_allocator_(multibuf_allocator),
connections_(allocator) {}
Gatt::~Gatt() { ResetConnections(); }
Result<Client> Gatt::CreateClient(ConnectionHandle connection_handle,
Client::Delegate& delegate) {
std::lock_guard lock(mutex_);
if (next_id_ == std::numeric_limits<uint16_t>::max()) {
return Status::ResourceExhausted();
}
internal::ClientId client_id{next_id_++};
auto conn_iter = FindOrInterceptAttChannel(connection_handle);
if (conn_iter == connections_.end()) {
return Status::Unavailable();
}
auto result = conn_iter->second.clients.try_emplace(
cpp23::to_underlying(client_id), &delegate);
if (!result.has_value()) {
return Status::Unavailable();
}
PW_CHECK(result->second);
return Client(client_id, connection_handle, *this);
}
Result<Server> Gatt::CreateServer(
ConnectionHandle connection_handle,
span<const CharacteristicInfo> characteristics,
Server::Delegate& delegate) {
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
// Ensure that no characteristics are already registered.
if (conn_iter != connections_.end()) {
for (auto characteristic : characteristics) {
if (conn_iter->second.characteristics.find(
cpp23::to_underlying(characteristic.value_handle)) !=
conn_iter->second.characteristics.end()) {
return Status::AlreadyExists();
}
}
}
if (next_id_ == std::numeric_limits<uint16_t>::max()) {
return Status::ResourceExhausted();
}
conn_iter = FindOrInterceptAttChannel(connection_handle);
if (conn_iter == connections_.end()) {
return Status::Unavailable();
}
internal::ServerId server_id{next_id_++};
auto server_result = conn_iter->second.servers.try_emplace(
cpp23::to_underlying(server_id), &delegate);
if (!server_result.has_value()) {
return Status::ResourceExhausted();
}
PW_CHECK(server_result->second);
CharacteristicMap characteristics_temp(allocator_);
for (const auto& characteristic : characteristics) {
auto result = characteristics_temp.try_emplace(
cpp23::to_underlying(characteristic.value_handle), server_id);
if (!result.has_value()) {
conn_iter->second.servers.erase(cpp23::to_underlying(server_id));
return Status::ResourceExhausted();
}
PW_CHECK(result->second);
}
conn_iter->second.characteristics.merge(characteristics_temp);
return Server(server_id, connection_handle, *this);
}
void Gatt::UnregisterClient(internal::ClientId client_id,
ConnectionHandle connection_handle) {
Client::Delegate* delegate = nullptr;
{
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return;
}
auto client_iter =
conn_iter->second.clients.find(cpp23::to_underlying(client_id));
if (client_iter == conn_iter->second.clients.end()) {
return;
}
for (auto iter = conn_iter->second.intercepted_notifications_.begin();
iter != conn_iter->second.intercepted_notifications_.end();) {
if (iter->second == client_id) {
iter = conn_iter->second.intercepted_notifications_.erase(iter);
continue;
}
++iter;
}
delegate = client_iter->second;
conn_iter->second.clients.erase(client_iter);
}
// Call outside of lock to avoid deadlock.
delegate->HandleError(Error::kClosedByClient, connection_handle);
// Leave connection/channel in connections_ map even if there are no clients
// remaining.
}
void Gatt::UnregisterServer(internal::ServerId server_id,
ConnectionHandle connection_handle) {
Server::Delegate* delegate = nullptr;
{
std::lock_guard queue_lock(write_available_mutex_);
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return;
}
auto server_iter =
conn_iter->second.servers.find(cpp23::to_underlying(server_id));
if (server_iter == conn_iter->second.servers.end()) {
return;
}
// Erase all characteristics owned by the server.
for (auto iter = conn_iter->second.characteristics.begin();
iter != conn_iter->second.characteristics.end();) {
if (iter->second != server_id) {
++iter;
continue;
}
iter = conn_iter->second.characteristics.erase(iter);
}
delegate = server_iter->second;
conn_iter->second.servers.erase(server_iter);
// Clean up write_available_queue_.
for (auto iter = write_available_queue_.begin();
iter != write_available_queue_.end();) {
if (iter->server_id == server_id) {
iter = write_available_queue_.erase(iter);
}
++iter;
}
}
// Call outside of lock to avoid deadlock.
delegate->HandleError(Error::kClosedByClient, connection_handle);
// Leave connection/channel in connections_ map even if there are no servers
// remaining.
}
Status Gatt::InterceptNotification(internal::ClientId client,
ConnectionHandle connection_handle,
AttributeHandle value_handle) {
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return Status::NotFound();
}
auto notification_iter =
pw::containers::Find(conn_iter->second.intercepted_notifications_,
std::make_pair(value_handle, client));
if (notification_iter != conn_iter->second.intercepted_notifications_.end()) {
return Status::AlreadyExists();
}
bool success = conn_iter->second.intercepted_notifications_.try_emplace_back(
value_handle, client);
if (!success) {
return Status::Unavailable();
}
return OkStatus();
}
Status Gatt::CancelInterceptNotification(internal::ClientId client,
ConnectionHandle connection_handle,
AttributeHandle value_handle) {
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return Status::NotFound();
}
auto notification_iter =
pw::containers::Find(conn_iter->second.intercepted_notifications_,
std::make_pair(value_handle, client));
if (notification_iter == conn_iter->second.intercepted_notifications_.end()) {
return Status::NotFound();
}
conn_iter->second.intercepted_notifications_.erase(notification_iter);
return OkStatus();
}
Result<UniquePtr<ChannelProxy>> Gatt::InterceptAttChannel(
ConnectionHandle connection_handle) {
return l2cap_.InterceptBasicModeChannel(
connection_handle,
static_cast<uint16_t>(emboss::L2capFixedCid::LE_U_ATTRIBUTE_PROTOCOL),
static_cast<uint16_t>(emboss::L2capFixedCid::LE_U_ATTRIBUTE_PROTOCOL),
AclTransportType::kLe,
pw::bind_member<&Gatt::OnSpanReceivedFromController>(this),
pw::bind_member<&Gatt::OnSpanReceivedFromHost>(this),
[this, connection_handle](L2capChannelEvent event) {
OnL2capEvent(event, connection_handle);
});
}
bool Gatt::OnSpanReceivedFromController(ConstByteSpan payload,
ConnectionHandle connection_handle,
uint16_t /*local_channel_id*/,
uint16_t /*remote_channel_id*/) {
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return false;
}
if (payload.size() < sizeof(emboss::AttOpcode)) {
return false;
}
const emboss::AttOpcode op_code{static_cast<uint8_t>(payload[0])};
PW_MODIFY_DIAGNOSTICS_PUSH();
PW_MODIFY_DIAGNOSTIC(ignored, "-Wswitch-enum");
switch (op_code) {
case emboss::AttOpcode::ATT_WRITE_CMD:
return OnAttWriteCmdFromController(payload, conn_iter);
case emboss::AttOpcode::ATT_HANDLE_VALUE_NTF:
return OnAttHandleValueNtfFromController(payload, conn_iter);
default:
return false;
}
PW_MODIFY_DIAGNOSTICS_POP();
}
bool Gatt::OnSpanReceivedFromHost(ConstByteSpan /*payload*/,
ConnectionHandle /*connection_handle*/,
uint16_t /*local_channel_id*/,
uint16_t /*remote_channel_id*/) {
// Intercepting outbound ATT packets is not supported.
return false;
}
void Gatt::OnL2capEvent(L2capChannelEvent event,
ConnectionHandle connection_handle) {
if (event == L2capChannelEvent::kReset) {
ResetConnections();
} else if (event == L2capChannelEvent::kChannelClosedByOther) {
OnChannelClosedEvent(connection_handle);
} else if (event == L2capChannelEvent::kWriteAvailable) {
OnWriteAvailable(connection_handle);
}
}
void Gatt::OnChannelClosedEvent(ConnectionHandle connection_handle) {
ClientMap closing_clients(allocator_);
ServerMap closing_servers(allocator_);
{
std::lock_guard queue_lock(write_available_mutex_);
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return;
}
Connection& conn = conn_iter->second;
closing_clients.swap(conn.clients);
closing_servers.swap(conn.servers);
connections_.erase(conn_iter);
// Clean up write_available_queue_
for (auto iter = write_available_queue_.begin();
iter != write_available_queue_.end();) {
if (iter->connection_handle == connection_handle) {
iter = write_available_queue_.erase(iter);
}
++iter;
}
}
// Notify delegates outside of mutex to avoid deadlock.
for (auto& [client_id, delegate] : closing_clients) {
delegate->HandleError(Error::kDisconnection, connection_handle);
}
// Notify delegates outside of mutex to avoid deadlock.
for (auto& [server_id, delegate] : closing_servers) {
delegate->HandleError(Error::kDisconnection, connection_handle);
}
}
void Gatt::OnWriteAvailable(ConnectionHandle connection_handle) {
std::lock_guard queue_lock(write_available_mutex_);
{
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return;
}
for (auto& [server_id, delegate] : conn_iter->second.servers) {
bool inserted =
write_available_queue_.try_emplace_back(QueuedWriteAvailable{
internal::ServerId{server_id}, connection_handle, delegate});
if (!inserted) {
PW_LOG_WARN(
"Cannot allocate write_available_queue_ item, unable to notify "
"more servers");
break;
}
}
}
// Call delegate outside of mutex_ lock so that clients can call
// Server::SendNotification() without deadlock.
for (uint16_t i = 0; i < write_available_queue_.size();) {
if (write_available_queue_[i].connection_handle == connection_handle) {
write_available_queue_[i].delegate->HandleWriteAvailable(
connection_handle);
write_available_queue_[i] = std::move(write_available_queue_.back());
write_available_queue_.pop_back();
} else {
++i;
}
}
}
void Gatt::ResetConnections() {
ConnectionMap closed_connections(allocator_);
{
std::lock_guard queue_lock(write_available_mutex_);
std::lock_guard lock(mutex_);
closed_connections.swap(connections_);
write_available_queue_.clear();
}
// Notify delegates outside of mutex to avoid deadlock.
for (auto& [handle, conn] : closed_connections) {
for (auto& [client_id, delegate] : conn.clients) {
delegate->HandleError(Error::kReset, ConnectionHandle{handle});
}
for (auto& [server_id, delegate] : conn.servers) {
delegate->HandleError(Error::kReset, ConnectionHandle{handle});
}
}
}
StatusWithMultiBuf Gatt::SendNotification(internal::ServerId server_id,
ConnectionHandle connection_handle,
AttributeHandle value_handle,
multibuf::MultiBuf&& value) {
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
PW_LOG_WARN(
"Attempt to send GATT notification for non-offloaded connection");
return {Status::FailedPrecondition(), std::move(value)};
}
auto char_iter = conn_iter->second.characteristics.find(
cpp23::to_underlying(value_handle));
if (char_iter == conn_iter->second.characteristics.end()) {
PW_LOG_WARN(
"Attempt to send GATT notification for non-offloaded attribute");
return {Status::FailedPrecondition(), std::move(value)};
}
if (char_iter->second != server_id) {
PW_LOG_WARN(
"Attempt to send GATT notification for attribute owned by different "
"server");
return {Status::InvalidArgument(), std::move(value)};
}
const size_t packet_size =
emboss::AttHandleValueNtf::MinSizeInBytes() + value.size();
std::optional<multibuf::MultiBuf> multibuf_result =
multibuf_allocator_.AllocateContiguous(packet_size);
if (!multibuf_result.has_value()) {
PW_LOG_WARN("Failed to allocate buffer for TX GATT notification");
return {Status::ResourceExhausted(), std::move(value)};
}
multibuf::MultiBuf multibuf = std::move(*multibuf_result);
span<uint8_t> multibuf_span =
span_cast<uint8_t>(multibuf.ContiguousSpan().value());
Result<emboss::AttHandleValueNtfWriter> writer =
MakeEmbossWriter<emboss::AttHandleValueNtfWriter>(
static_cast<int32_t>(value.size()), &multibuf_span);
PW_CHECK(writer.ok());
writer->attribute_opcode().Write(emboss::AttOpcode::ATT_HANDLE_VALUE_NTF);
writer->attribute_handle().Write(cpp23::to_underlying(value_handle));
span<uint8_t> attribute_bytes(
writer->attribute_value().BackingStorage().data(),
writer->attribute_value().SizeInBytes());
PW_CHECK_OK(value.CopyTo(as_writable_bytes(attribute_bytes)));
StatusWithMultiBuf write_result =
conn_iter->second.att_channel->Write(std::move(multibuf));
if (!write_result.status.ok()) {
return {write_result.status, std::move(value)};
}
return {OkStatus()};
}
Gatt::ConnectionMap::iterator Gatt::FindOrInterceptAttChannel(
ConnectionHandle connection_handle) {
auto result = connections_.try_emplace(
cpp23::to_underlying(connection_handle), allocator_);
if (!result.has_value()) {
return connections_.end();
}
auto [conn_iter, inserted] = result.value();
if (!inserted) {
return conn_iter;
}
Result<UniquePtr<ChannelProxy>> channel_result =
InterceptAttChannel(connection_handle);
if (!channel_result.ok()) {
connections_.erase(conn_iter);
return connections_.end();
}
conn_iter->second.att_channel = std::move(channel_result.value());
return conn_iter;
}
bool Gatt::OnAttHandleValueNtfFromController(
ConstByteSpan payload, ConnectionMap::iterator conn_iter) {
const size_t attribute_size =
payload.size() - emboss::AttHandleValueNtf::MinSizeInBytes();
Result<emboss::AttHandleValueNtfView> view =
MakeEmbossView<emboss::AttHandleValueNtfView>(
static_cast<int32_t>(attribute_size),
reinterpret_cast<const uint8_t*>(payload.data()),
payload.size());
if (!view.ok()) {
PW_LOG_WARN("Received invalid ATT_HANDLE_VALUE_NTF");
return false;
}
PW_CHECK(view->attribute_opcode().Read() ==
emboss::AttOpcode::ATT_HANDLE_VALUE_NTF);
AttributeHandle att_handle{view->attribute_handle().Read()};
bool intercepted = false;
for (auto& [intercepted_handle, client_id] :
conn_iter->second.intercepted_notifications_) {
if (att_handle != intercepted_handle) {
continue;
}
intercepted = true;
auto client_iter =
conn_iter->second.clients.find(cpp23::to_underlying(client_id));
PW_CHECK(client_iter != conn_iter->second.clients.end());
std::optional<multibuf::MultiBuf> buffer =
multibuf_allocator_.AllocateContiguous(attribute_size);
if (!buffer.has_value()) {
PW_LOG_WARN("Failed to allocate multibuf for attribute value");
return true;
}
pw::span<const uint8_t> backing_storage(
view->attribute_value().BackingStorage().data(),
view->attribute_value().SizeInBytes());
auto bytes_copied = buffer->CopyFrom(as_bytes(backing_storage));
PW_CHECK(bytes_copied.ok());
PW_CHECK_UINT_EQ(bytes_copied.size(), attribute_size);
client_iter->second->HandleNotification(
ConnectionHandle{conn_iter->first}, att_handle, std::move(*buffer));
}
return intercepted;
}
bool Gatt::OnAttWriteCmdFromController(ConstByteSpan payload,
ConnectionMap::iterator conn_iter) {
if (payload.size() < emboss::AttWriteCmd::MinSizeInBytes()) {
PW_LOG_WARN("Received invalid ATT_WRITE_CMD");
return false;
}
const size_t attribute_size =
payload.size() - emboss::AttWriteCmd::MinSizeInBytes();
Result<emboss::AttWriteCmdView> view =
MakeEmbossView<emboss::AttWriteCmdView>(
static_cast<int32_t>(attribute_size),
reinterpret_cast<const uint8_t*>(payload.data()),
payload.size());
if (!view.ok()) {
PW_LOG_WARN("Received invalid ATT_WRITE_CMD");
return false;
}
PW_CHECK(view->attribute_opcode().Read() == emboss::AttOpcode::ATT_WRITE_CMD);
AttributeHandle att_handle{view->attribute_handle().Read()};
auto char_iter =
conn_iter->second.characteristics.find(cpp23::to_underlying(att_handle));
if (char_iter == conn_iter->second.characteristics.end()) {
return false;
}
auto server_iter =
conn_iter->second.servers.find(cpp23::to_underlying(char_iter->second));
PW_CHECK(server_iter != conn_iter->second.servers.end());
std::optional<multibuf::MultiBuf> buffer =
multibuf_allocator_.AllocateContiguous(attribute_size);
if (!buffer.has_value()) {
PW_LOG_WARN("Failed to allocate multibuf for attribute value");
return true;
}
pw::span<const uint8_t> backing_storage(
view->attribute_value().BackingStorage().data(),
view->attribute_value().SizeInBytes());
auto bytes_copied = buffer->CopyFrom(as_bytes(backing_storage));
PW_CHECK(bytes_copied.ok());
PW_CHECK_UINT_EQ(bytes_copied.size(), attribute_size);
server_iter->second->HandleWriteWithoutResponse(
ConnectionHandle{conn_iter->first}, att_handle, std::move(*buffer));
// The command was intercepted.
return true;
}
Status Gatt::AddCharacteristic(internal::ServerId server_id,
ConnectionHandle connection_handle,
CharacteristicInfo characteristic) {
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return Status::FailedPrecondition();
}
auto server_iter =
conn_iter->second.servers.find(cpp23::to_underlying(server_id));
if (server_iter == conn_iter->second.servers.end()) {
return Status::FailedPrecondition();
}
auto result = conn_iter->second.characteristics.try_emplace(
cpp23::to_underlying(characteristic.value_handle), server_id);
if (!result.has_value()) {
return Status::ResourceExhausted();
}
if (!result->second) {
return Status::AlreadyExists();
}
return OkStatus();
}
Status Gatt::RemoveCharacteristic(internal::ServerId server_id,
ConnectionHandle connection_handle,
CharacteristicInfo characteristic) {
std::lock_guard lock(mutex_);
auto conn_iter = connections_.find(cpp23::to_underlying(connection_handle));
if (conn_iter == connections_.end()) {
return Status::FailedPrecondition();
}
auto char_iter = conn_iter->second.characteristics.find(
cpp23::to_underlying(characteristic.value_handle));
if (char_iter == conn_iter->second.characteristics.end() ||
char_iter->second != server_id) {
return Status::NotFound();
}
conn_iter->second.characteristics.erase(char_iter);
return OkStatus();
}
} // namespace pw::bluetooth::proxy::gatt