| // Copyright 2023 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/internal/host/gatt/client.h" |
| |
| #include "pw_bluetooth_sapphire/internal/host/att/att.h" |
| #include "pw_bluetooth_sapphire/internal/host/common/assert.h" |
| #include "pw_bluetooth_sapphire/internal/host/common/log.h" |
| #include "pw_bluetooth_sapphire/internal/host/common/slab_allocator.h" |
| #include "pw_bluetooth_sapphire/internal/host/common/trace.h" |
| #include "pw_bluetooth_sapphire/internal/host/gatt/gatt_defs.h" |
| |
| #pragma clang diagnostic ignored "-Wshadow" |
| |
| using bt::HostError; |
| |
| namespace bt::gatt { |
| namespace { |
| |
| MutableByteBufferPtr NewPDU(size_t param_size) { |
| auto pdu = NewBuffer(sizeof(att::Header) + param_size); |
| if (!pdu) { |
| bt_log(DEBUG, "att", "out of memory"); |
| } |
| return pdu; |
| } |
| |
| template <att::UUIDType Format, |
| typename EntryType = att::InformationData<Format>> |
| bool ProcessDescriptorDiscoveryResponse( |
| att::Handle range_start, |
| att::Handle range_end, |
| BufferView entries, |
| Client::DescriptorCallback desc_callback, |
| att::Handle* out_last_handle) { |
| BT_DEBUG_ASSERT(out_last_handle); |
| |
| if (entries.size() % sizeof(EntryType)) { |
| bt_log(DEBUG, "gatt", "malformed information data list"); |
| return false; |
| } |
| |
| att::Handle last_handle = range_end; |
| while (entries.size()) { |
| const EntryType& entry = entries.To<EntryType>(); |
| |
| att::Handle desc_handle = le16toh(entry.handle); |
| |
| // Stop and report an error if the server erroneously responds with |
| // an attribute outside the requested range. |
| if (desc_handle > range_end || desc_handle < range_start) { |
| bt_log(DEBUG, |
| "gatt", |
| "descriptor handle out of range (handle: %#.4x, " |
| "range: %#.4x - %#.4x)", |
| desc_handle, |
| range_start, |
| range_end); |
| return false; |
| } |
| |
| // The handles must be strictly increasing. |
| if (last_handle != range_end && desc_handle <= last_handle) { |
| bt_log(DEBUG, "gatt", "descriptor handles not strictly increasing"); |
| return false; |
| } |
| |
| last_handle = desc_handle; |
| |
| // Notify the handler. |
| desc_callback(DescriptorData(desc_handle, UUID(entry.uuid))); |
| |
| entries = entries.view(sizeof(EntryType)); |
| } |
| |
| *out_last_handle = last_handle; |
| return true; |
| } |
| |
| } // namespace |
| |
| class Impl final : public Client { |
| public: |
| explicit Impl(att::Bearer::WeakPtr bearer) |
| : att_(std::move(bearer)), weak_self_(this) { |
| BT_DEBUG_ASSERT(att_.is_alive()); |
| |
| auto handler = [this](auto txn_id, const att::PacketReader& pdu) { |
| BT_DEBUG_ASSERT(pdu.opcode() == att::kNotification || |
| pdu.opcode() == att::kIndication); |
| |
| if (pdu.payload_size() < sizeof(att::NotificationParams)) { |
| // Received a malformed notification. Disconnect the link. |
| bt_log(DEBUG, "gatt", "malformed notification/indication PDU"); |
| att_->ShutDown(); |
| return; |
| } |
| |
| bool is_ind = pdu.opcode() == att::kIndication; |
| const auto& params = pdu.payload<att::NotificationParams>(); |
| att::Handle handle = le16toh(params.handle); |
| size_t value_size = pdu.payload_size() - sizeof(att::Handle); |
| |
| // Auto-confirm indications. |
| if (is_ind) { |
| auto pdu = NewPDU(0u); |
| if (pdu) { |
| att::PacketWriter(att::kConfirmation, pdu.get()); |
| att_->Reply(txn_id, std::move(pdu)); |
| } else { |
| att_->ReplyWithError( |
| txn_id, handle, att::ErrorCode::kInsufficientResources); |
| } |
| } |
| |
| bool maybe_truncated = false; |
| // If the value is the max size that fits in the MTU, it may be truncated. |
| if (value_size == |
| att_->mtu() - sizeof(att::OpCode) - sizeof(att::Handle)) { |
| maybe_truncated = true; |
| } |
| |
| // Run the handler |
| if (notification_handler_) { |
| notification_handler_(is_ind, |
| handle, |
| BufferView(params.value, value_size), |
| maybe_truncated); |
| } else { |
| bt_log( |
| TRACE, "gatt", "dropped notification/indication without handler"); |
| } |
| }; |
| |
| not_handler_id_ = att_->RegisterHandler(att::kNotification, handler); |
| ind_handler_id_ = att_->RegisterHandler(att::kIndication, handler); |
| } |
| |
| ~Impl() override { |
| att_->UnregisterHandler(not_handler_id_); |
| att_->UnregisterHandler(ind_handler_id_); |
| } |
| |
| using WeakPtr = WeakSelf<Client>::WeakPtr; |
| WeakPtr GetWeakPtr() override { return weak_self_.GetWeakPtr(); } |
| |
| private: |
| uint16_t mtu() const override { return att_->mtu(); } |
| |
| void ExchangeMTU(MTUCallback mtu_cb) override { |
| auto pdu = NewPDU(sizeof(att::ExchangeMTURequestParams)); |
| if (!pdu) { |
| mtu_cb(fit::error(att::Error(HostError::kOutOfMemory))); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kExchangeMTURequest, pdu.get()); |
| auto params = writer.mutable_payload<att::ExchangeMTURequestParams>(); |
| params->client_rx_mtu = htole16(att_->preferred_mtu()); |
| |
| auto rsp_cb = [this, mtu_cb = std::move(mtu_cb)]( |
| att::Bearer::TransactionResult result) mutable { |
| if (result.is_ok()) { |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kExchangeMTUResponse); |
| |
| if (rsp.payload_size() != sizeof(att::ExchangeMTUResponseParams)) { |
| // Received a malformed response. Disconnect the link. |
| att_->ShutDown(); |
| |
| mtu_cb(fit::error(att::Error(HostError::kPacketMalformed))); |
| return; |
| } |
| |
| const auto& rsp_params = rsp.payload<att::ExchangeMTUResponseParams>(); |
| uint16_t server_mtu = le16toh(rsp_params.server_rx_mtu); |
| |
| // If the minimum value is less than the default MTU, then go with the |
| // default MTU (Vol 3, Part F, 3.4.2.2). |
| uint16_t final_mtu = std::max( |
| att::kLEMinMTU, std::min(server_mtu, att_->preferred_mtu())); |
| att_->set_mtu(final_mtu); |
| |
| mtu_cb(fit::ok(final_mtu)); |
| return; |
| } |
| const auto& [error, handle] = result.error_value(); |
| |
| // "If the Error Response is sent by the server with the Error Code |
| // set to Request Not Supported, [...] the default MTU shall be used |
| // (Vol 3, Part G, 4.3.1)" |
| if (error.is(att::ErrorCode::kRequestNotSupported)) { |
| bt_log( |
| DEBUG, "gatt", "peer does not support MTU exchange: using default"); |
| att_->set_mtu(att::kLEMinMTU); |
| mtu_cb(fit::error(error)); |
| return; |
| } |
| |
| bt_log(DEBUG, "gatt", "MTU exchange failed: %s", bt_str(error)); |
| mtu_cb(fit::error(error)); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void DiscoverServices(ServiceKind kind, |
| ServiceCallback svc_callback, |
| att::ResultFunction<> status_callback) override { |
| DiscoverServicesInRange(kind, |
| att::kHandleMin, |
| att::kHandleMax, |
| std::move(svc_callback), |
| std::move(status_callback)); |
| } |
| |
| void DiscoverServicesInRange(ServiceKind kind, |
| att::Handle range_start, |
| att::Handle range_end, |
| ServiceCallback svc_callback, |
| att::ResultFunction<> status_callback) override { |
| BT_ASSERT(range_start <= range_end); |
| |
| auto pdu = NewPDU(sizeof(att::ReadByGroupTypeRequestParams16)); |
| if (!pdu) { |
| status_callback(ToResult(HostError::kOutOfMemory)); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kReadByGroupTypeRequest, pdu.get()); |
| auto* params = |
| writer.mutable_payload<att::ReadByGroupTypeRequestParams16>(); |
| params->start_handle = htole16(range_start); |
| params->end_handle = htole16(range_end); |
| params->type = |
| htole16(kind == ServiceKind::PRIMARY ? types::kPrimaryService16 |
| : types::kSecondaryService16); |
| |
| auto rsp_cb = [this, |
| kind, |
| range_start, |
| range_end, |
| svc_cb = std::move(svc_callback), |
| res_cb = std::move(status_callback)]( |
| att::Bearer::TransactionResult result) mutable { |
| if (result.is_error()) { |
| const att::Error& error = result.error_value().first; |
| |
| // An Error Response code of "Attribute Not Found" indicates the end of |
| // the procedure (v5.0, Vol 3, Part G, 4.4.1). |
| if (error.is(att::ErrorCode::kAttributeNotFound)) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| res_cb(fit::error(error)); |
| return; |
| } |
| |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kReadByGroupTypeResponse); |
| TRACE_DURATION("bluetooth", |
| "gatt::Client::DiscoverServicesInRange rsp_cb", |
| "size", |
| rsp.size()); |
| |
| if (rsp.payload_size() < sizeof(att::ReadByGroupTypeResponseParams)) { |
| // Received malformed response. Disconnect the link. |
| bt_log(DEBUG, "gatt", "received malformed Read By Group Type response"); |
| att_->ShutDown(); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| const auto& rsp_params = |
| rsp.payload<att::ReadByGroupTypeResponseParams>(); |
| uint8_t entry_length = rsp_params.length; |
| |
| // We expect the returned attribute value to be a 16-bit or 128-bit |
| // service UUID. |
| constexpr size_t kAttrDataSize16 = |
| sizeof(att::AttributeGroupDataEntry) + sizeof(att::AttributeType16); |
| constexpr size_t kAttrDataSize128 = |
| sizeof(att::AttributeGroupDataEntry) + sizeof(att::AttributeType128); |
| |
| if (entry_length != kAttrDataSize16 && entry_length != kAttrDataSize128) { |
| bt_log(DEBUG, "gatt", "invalid attribute data length"); |
| att_->ShutDown(); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| BufferView attr_data_list(rsp_params.attribute_data_list, |
| rsp.payload_size() - 1); |
| if (attr_data_list.size() % entry_length) { |
| bt_log(DEBUG, "gatt", "malformed attribute data list"); |
| att_->ShutDown(); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| std::optional<att::Handle> last_handle; |
| while (attr_data_list.size()) { |
| att::Handle start = le16toh( |
| attr_data_list |
| .ReadMember<&att::AttributeGroupDataEntry::start_handle>()); |
| att::Handle end = le16toh( |
| attr_data_list |
| .ReadMember<&att::AttributeGroupDataEntry::group_end_handle>()); |
| |
| if (end < start) { |
| bt_log(DEBUG, "gatt", "received malformed service range values"); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| if (start < range_start || start > range_end) { |
| bt_log(DEBUG, |
| "gatt", |
| "received service range values outside of requested range"); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| // "The Attribute Data List is ordered sequentially based on the |
| // attribute handles." (Core Spec v5.3, Vol 3, Part F, Sec 3.4.4.10) |
| if (last_handle.has_value() && start <= last_handle.value()) { |
| bt_log(DEBUG, "gatt", "received services out of order"); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| // This must succeed as we have performed the appropriate checks above. |
| auto uuid_bytes = |
| attr_data_list.view(offsetof(att::AttributeGroupDataEntry, value), |
| entry_length - (2 * sizeof(att::Handle))); |
| UUID uuid(uuid_bytes); |
| |
| ServiceData service(kind, start, end, uuid); |
| last_handle = service.range_end; |
| |
| // Notify the handler. |
| svc_cb(service); |
| |
| attr_data_list = attr_data_list.view(entry_length); |
| } |
| |
| // The procedure is over if we have reached the end of the handle range. |
| if (!last_handle.has_value() || last_handle.value() == range_end) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| // Request the next batch. |
| DiscoverServicesInRange(kind, |
| last_handle.value() + 1, |
| range_end, |
| std::move(svc_cb), |
| std::move(res_cb)); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void DiscoverServicesWithUuids(ServiceKind kind, |
| ServiceCallback svc_cb, |
| att::ResultFunction<> status_cb, |
| std::vector<UUID> uuids) override { |
| DiscoverServicesWithUuidsInRange(kind, |
| att::kHandleMin, |
| att::kHandleMax, |
| std::move(svc_cb), |
| std::move(status_cb), |
| std::move(uuids)); |
| } |
| |
| void DiscoverServicesWithUuidsInRange(ServiceKind kind, |
| att::Handle range_start, |
| att::Handle range_end, |
| ServiceCallback svc_callback, |
| att::ResultFunction<> status_callback, |
| std::vector<UUID> uuids) override { |
| BT_ASSERT(range_start <= range_end); |
| BT_ASSERT(!uuids.empty()); |
| UUID uuid = uuids.back(); |
| uuids.pop_back(); |
| |
| auto recursive_status_cb = [this, |
| range_start, |
| range_end, |
| kind, |
| svc_cb = svc_callback.share(), |
| status_cb = std::move(status_callback), |
| remaining_uuids = |
| std::move(uuids)](auto status) mutable { |
| // Base case |
| if (status.is_error() || remaining_uuids.empty()) { |
| status_cb(status); |
| return; |
| } |
| |
| // Recursively discover with the remaining UUIDs. |
| DiscoverServicesWithUuidsInRange(kind, |
| range_start, |
| range_end, |
| std::move(svc_cb), |
| std::move(status_cb), |
| std::move(remaining_uuids)); |
| }; |
| |
| // Discover the last uuid in uuids. |
| DiscoverServicesByUuidInRange(kind, |
| range_start, |
| range_end, |
| std::move(svc_callback), |
| std::move(recursive_status_cb), |
| uuid); |
| } |
| |
| void DiscoverServicesByUuidInRange(ServiceKind kind, |
| att::Handle start, |
| att::Handle end, |
| ServiceCallback svc_callback, |
| att::ResultFunction<> status_callback, |
| UUID uuid) { |
| size_t uuid_size_bytes = uuid.CompactSize(/* allow 32 bit UUIDs */ false); |
| auto pdu = |
| NewPDU(sizeof(att::FindByTypeValueRequestParams) + uuid_size_bytes); |
| if (!pdu) { |
| status_callback(ToResult(HostError::kOutOfMemory)); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kFindByTypeValueRequest, pdu.get()); |
| auto* params = writer.mutable_payload<att::FindByTypeValueRequestParams>(); |
| params->start_handle = htole16(start); |
| params->end_handle = htole16(end); |
| params->type = |
| htole16(kind == ServiceKind::PRIMARY ? types::kPrimaryService16 |
| : types::kSecondaryService16); |
| MutableBufferView value_view(params->value, uuid_size_bytes); |
| uuid.ToBytes(&value_view, /* allow 32 bit UUIDs */ false); |
| |
| auto rsp_cb = [this, |
| kind, |
| discovery_range_start = start, |
| discovery_range_end = end, |
| svc_cb = std::move(svc_callback), |
| res_cb = std::move(status_callback), |
| uuid](att::Bearer::TransactionResult result) mutable { |
| if (result.is_error()) { |
| const att::Error& error = result.error_value().first; |
| |
| // An Error Response code of "Attribute Not Found" indicates the end of |
| // the procedure (v5.0, Vol 3, Part G, 4.4.2). |
| if (error.is(att::ErrorCode::kAttributeNotFound)) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| res_cb(fit::error(error)); |
| return; |
| } |
| |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kFindByTypeValueResponse); |
| |
| size_t payload_size = rsp.payload_size(); |
| if (payload_size < 1 || |
| payload_size % sizeof(att::FindByTypeValueResponseParams) != 0) { |
| // Received malformed response. Disconnect the link. |
| bt_log(DEBUG, |
| "gatt", |
| "received malformed Find By Type Value response with size %zu", |
| payload_size); |
| att_->ShutDown(); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| BufferView handle_list = rsp.payload_data(); |
| |
| std::optional<att::Handle> last_handle; |
| while (handle_list.size()) { |
| const auto& entry = handle_list.To<att::HandlesInformationList>(); |
| |
| att::Handle start = le16toh(entry.handle); |
| att::Handle end = le16toh(entry.group_end_handle); |
| |
| if (end < start) { |
| bt_log(DEBUG, "gatt", "received malformed service range values"); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| if (start < discovery_range_start || start > discovery_range_end) { |
| bt_log(DEBUG, |
| "gatt", |
| "received service range values outside of requested range"); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| // "The Handles Information List is ordered sequentially based on the |
| // found attribute handles." (Core Spec v5.3, Vol 3, Part F, |
| // Sec 3.4.3.4) |
| if (last_handle.has_value() && start <= last_handle.value()) { |
| bt_log(DEBUG, "gatt", "received services out of order"); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| ServiceData service(kind, start, end, uuid); |
| |
| // Notify the handler. |
| svc_cb(service); |
| |
| // HandlesInformationList is a single element of the list. |
| size_t entry_length = sizeof(att::HandlesInformationList); |
| handle_list = handle_list.view(entry_length); |
| |
| last_handle = service.range_end; |
| } |
| |
| // The procedure is over if we have reached the end of the handle range. |
| if (!last_handle.has_value() || |
| last_handle.value() == discovery_range_end) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| // Request the next batch. |
| DiscoverServicesByUuidInRange(kind, |
| last_handle.value() + 1, |
| discovery_range_end, |
| std::move(svc_cb), |
| std::move(res_cb), |
| uuid); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void DiscoverCharacteristics(att::Handle range_start, |
| att::Handle range_end, |
| CharacteristicCallback chrc_callback, |
| att::ResultFunction<> status_callback) override { |
| BT_ASSERT(range_start <= range_end); |
| BT_ASSERT(chrc_callback); |
| BT_ASSERT(status_callback); |
| |
| if (range_start == range_end) { |
| status_callback(fit::ok()); |
| return; |
| } |
| |
| auto read_by_type_cb = [this, |
| range_end, |
| chrc_cb = std::move(chrc_callback), |
| res_cb = status_callback.share()]( |
| ReadByTypeResult result) mutable { |
| TRACE_DURATION("bluetooth", |
| "gatt::Client::DiscoverCharacteristics read_by_type_cb"); |
| |
| if (result.is_error()) { |
| const auto error = result.error_value().error; |
| |
| // An Error Response code of "Attribute Not Found" indicates the end |
| // of the procedure (v5.0, Vol 3, Part G, 4.6.1). |
| if (error.is(att::ErrorCode::kAttributeNotFound)) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| res_cb(fit::error(error)); |
| return; |
| } |
| |
| auto& attributes = result.value(); |
| |
| // ReadByTypeRequest() should return an error result if there are no |
| // attributes in a success response. |
| BT_ASSERT(!attributes.empty()); |
| |
| for (auto& char_attr : attributes) { |
| Properties properties = 0u; |
| att::Handle value_handle = 0u; |
| UUID value_uuid; |
| |
| // The characteristic declaration value contains: |
| // 1 octet: properties |
| // 2 octets: value handle |
| // 2 or 16 octets: UUID |
| if (char_attr.value.size() == |
| sizeof(CharacteristicDeclarationAttributeValue< |
| att::UUIDType::k16Bit>)) { |
| auto attr_value = char_attr.value.To< |
| CharacteristicDeclarationAttributeValue<att::UUIDType::k16Bit>>(); |
| properties = attr_value.properties; |
| value_handle = le16toh(attr_value.value_handle); |
| value_uuid = UUID(attr_value.value_uuid); |
| } else if (char_attr.value.size() == |
| sizeof(CharacteristicDeclarationAttributeValue< |
| att::UUIDType::k128Bit>)) { |
| auto attr_value = |
| char_attr.value.To<CharacteristicDeclarationAttributeValue< |
| att::UUIDType::k128Bit>>(); |
| properties = attr_value.properties; |
| value_handle = le16toh(attr_value.value_handle); |
| value_uuid = UUID(attr_value.value_uuid); |
| } else { |
| bt_log(DEBUG, |
| "gatt", |
| "invalid characteristic declaration attribute value size"); |
| att_->ShutDown(); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| // Vol 3, Part G, 3.3: "The Characteristic Value declaration shall |
| // exist immediately following the characteristic declaration." |
| if (value_handle != char_attr.handle + 1) { |
| bt_log( |
| DEBUG, "gatt", "characteristic value doesn't follow declaration"); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| // Notify the handler. By default, there are no extended properties to |
| // report. |
| chrc_cb(CharacteristicData(properties, |
| /*ext_props=*/std::nullopt, |
| char_attr.handle, |
| value_handle, |
| value_uuid)); |
| } |
| |
| // The procedure is over if we have reached the end of the handle |
| // range. |
| const auto last_handle = attributes.back().handle; |
| if (last_handle == range_end) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| // Request the next batch. |
| DiscoverCharacteristics( |
| last_handle + 1, range_end, std::move(chrc_cb), std::move(res_cb)); |
| }; |
| |
| ReadByTypeRequest(types::kCharacteristicDeclaration, |
| range_start, |
| range_end, |
| std::move(read_by_type_cb)); |
| } |
| |
| void DiscoverDescriptors(att::Handle range_start, |
| att::Handle range_end, |
| DescriptorCallback desc_callback, |
| att::ResultFunction<> status_callback) override { |
| BT_DEBUG_ASSERT(range_start <= range_end); |
| BT_DEBUG_ASSERT(desc_callback); |
| BT_DEBUG_ASSERT(status_callback); |
| |
| auto pdu = NewPDU(sizeof(att::FindInformationRequestParams)); |
| if (!pdu) { |
| status_callback(ToResult(HostError::kOutOfMemory)); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kFindInformationRequest, pdu.get()); |
| auto* params = writer.mutable_payload<att::FindInformationRequestParams>(); |
| params->start_handle = htole16(range_start); |
| params->end_handle = htole16(range_end); |
| |
| auto rsp_cb = [this, |
| range_start, |
| range_end, |
| desc_cb = std::move(desc_callback), |
| res_cb = std::move(status_callback)]( |
| att::Bearer::TransactionResult result) mutable { |
| if (result.is_error()) { |
| const att::Error& error = result.error_value().first; |
| |
| // An Error Response code of "Attribute Not Found" indicates the end of |
| // the procedure (v5.0, Vol 3, Part G, 4.7.1). |
| if (error.is(att::ErrorCode::kAttributeNotFound)) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| res_cb(fit::error(error)); |
| return; |
| } |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kFindInformationResponse); |
| TRACE_DURATION("bluetooth", |
| "gatt::Client::DiscoverDescriptors rsp_cb", |
| "size", |
| rsp.size()); |
| |
| if (rsp.payload_size() < sizeof(att::FindInformationResponseParams)) { |
| bt_log(DEBUG, "gatt", "received malformed Find Information response"); |
| att_->ShutDown(); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| const auto& rsp_params = |
| rsp.payload<att::FindInformationResponseParams>(); |
| BufferView entries = rsp.payload_data().view(sizeof(rsp_params.format)); |
| |
| att::Handle last_handle; |
| bool well_formed; |
| switch (rsp_params.format) { |
| case att::UUIDType::k16Bit: |
| well_formed = |
| ProcessDescriptorDiscoveryResponse<att::UUIDType::k16Bit>( |
| range_start, |
| range_end, |
| entries, |
| desc_cb.share(), |
| &last_handle); |
| break; |
| case att::UUIDType::k128Bit: |
| well_formed = |
| ProcessDescriptorDiscoveryResponse<att::UUIDType::k128Bit>( |
| range_start, |
| range_end, |
| entries, |
| desc_cb.share(), |
| &last_handle); |
| break; |
| default: |
| bt_log(DEBUG, "gatt", "invalid information data format"); |
| well_formed = false; |
| break; |
| } |
| |
| if (!well_formed) { |
| att_->ShutDown(); |
| res_cb(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| // The procedure is over if we have reached the end of the handle range. |
| if (last_handle == range_end) { |
| res_cb(fit::ok()); |
| return; |
| } |
| |
| // Request the next batch. |
| DiscoverDescriptors( |
| last_handle + 1, range_end, std::move(desc_cb), std::move(res_cb)); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void ReadRequest(att::Handle handle, ReadCallback callback) override { |
| auto pdu = NewPDU(sizeof(att::ReadRequestParams)); |
| if (!pdu) { |
| callback(ToResult(HostError::kOutOfMemory), |
| BufferView(), |
| /*maybe_truncated=*/false); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kReadRequest, pdu.get()); |
| auto params = writer.mutable_payload<att::ReadRequestParams>(); |
| params->handle = htole16(handle); |
| |
| auto rsp_cb = [this, callback = std::move(callback)]( |
| att::Bearer::TransactionResult result) { |
| if (result.is_ok()) { |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kReadResponse); |
| bool maybe_truncated = |
| (rsp.payload_size() != att::kMaxAttributeValueLength) && |
| (rsp.payload_size() == (mtu() - sizeof(rsp.opcode()))); |
| callback(fit::ok(), rsp.payload_data(), maybe_truncated); |
| return; |
| } |
| const auto& [error, handle] = result.error_value(); |
| bt_log(DEBUG, |
| "gatt", |
| "read request failed: %s, handle %#.4x", |
| bt_str(error), |
| handle); |
| callback(fit::error(error), BufferView(), /*maybe_truncated=*/false); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void ReadByTypeRequest(const UUID& type, |
| att::Handle start_handle, |
| att::Handle end_handle, |
| ReadByTypeCallback callback) override { |
| size_t type_size = type.CompactSize(/*allow_32bit=*/false); |
| BT_ASSERT(type_size == sizeof(uint16_t) || type_size == sizeof(UInt128)); |
| auto pdu = NewPDU(type_size == sizeof(uint16_t) |
| ? sizeof(att::ReadByTypeRequestParams16) |
| : sizeof(att::ReadByTypeRequestParams128)); |
| if (!pdu) { |
| callback(fit::error( |
| ReadByTypeError{Error(HostError::kOutOfMemory), std::nullopt})); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kReadByTypeRequest, pdu.get()); |
| if (type_size == sizeof(uint16_t)) { |
| auto params = writer.mutable_payload<att::ReadByTypeRequestParams16>(); |
| params->start_handle = htole16(start_handle); |
| params->end_handle = htole16(end_handle); |
| auto type_view = MutableBufferView(¶ms->type, sizeof(params->type)); |
| type.ToBytes(&type_view, /*allow_32bit=*/false); |
| } else { |
| auto params = writer.mutable_payload<att::ReadByTypeRequestParams128>(); |
| params->start_handle = htole16(start_handle); |
| params->end_handle = htole16(end_handle); |
| auto type_view = MutableBufferView(¶ms->type, sizeof(params->type)); |
| type.ToBytes(&type_view, /*allow_32bit=*/false); |
| } |
| |
| auto rsp_cb = [this, |
| callback = std::move(callback), |
| start_handle, |
| end_handle](att::Bearer::TransactionResult result) { |
| if (result.is_error()) { |
| const auto& [error, handle] = result.error_value(); |
| bt_log(DEBUG, |
| "gatt", |
| "read by type request failed: %s, handle %#.4x", |
| bt_str(error), |
| handle); |
| // Only some errors have handles. |
| std::optional<att::Handle> cb_handle = |
| handle ? std::optional(handle) : std::nullopt; |
| callback(fit::error(ReadByTypeError{error, cb_handle})); |
| return; |
| } |
| const att::PacketReader& rsp = result.value(); |
| BT_ASSERT(rsp.opcode() == att::kReadByTypeResponse); |
| if (rsp.payload_size() < sizeof(att::ReadByTypeResponseParams)) { |
| callback(fit::error( |
| ReadByTypeError{Error(HostError::kPacketMalformed), std::nullopt})); |
| return; |
| } |
| |
| const auto& params = rsp.payload<att::ReadByTypeResponseParams>(); |
| // The response contains a list of attribute handle-value pairs of uniform |
| // length. |
| const size_t list_size = rsp.payload_size() - sizeof(params.length); |
| const size_t pair_size = params.length; |
| |
| // Success response must: |
| // a) Specify valid pair length (at least the size of a handle). |
| // b) Have at least 1 pair (otherwise the Attribute Not Found error should |
| // have been |
| // sent). |
| // c) Have a list size that is evenly divisible by pair size. |
| if (pair_size < sizeof(att::Handle) || list_size < sizeof(att::Handle) || |
| list_size % pair_size != 0) { |
| callback(fit::error( |
| ReadByTypeError{Error(HostError::kPacketMalformed), std::nullopt})); |
| return; |
| } |
| |
| std::vector<ReadByTypeValue> attributes; |
| BufferView attr_list_view(params.attribute_data_list, |
| rsp.payload_size() - sizeof(params.length)); |
| while (attr_list_view.size() >= params.length) { |
| const BufferView pair_view = attr_list_view.view(0, pair_size); |
| const att::Handle handle = le16toh(pair_view.To<att::Handle>()); |
| |
| if (handle < start_handle || handle > end_handle) { |
| bt_log(TRACE, |
| "gatt", |
| "client received read by type response with handle outside of " |
| "requested range"); |
| callback(fit::error(ReadByTypeError{ |
| Error(HostError::kPacketMalformed), std::nullopt})); |
| return; |
| } |
| |
| if (!attributes.empty() && attributes.back().handle >= handle) { |
| bt_log(TRACE, |
| "gatt", |
| "client received read by type response with handles in " |
| "non-increasing order"); |
| callback(fit::error(ReadByTypeError{ |
| Error(HostError::kPacketMalformed), std::nullopt})); |
| return; |
| } |
| |
| auto value_view = pair_view.view(sizeof(att::Handle)); |
| |
| // The value may be truncated if it maxes out the length parameter or |
| // the MTU, whichever is smaller (Core Spec v5.2, Vol 3, Part F, |
| // Sec 3.4.4). |
| const size_t mtu_max_value_size = |
| mtu() - sizeof(att::kReadByTypeResponse) - |
| sizeof(att::ReadByTypeResponseParams) - sizeof(att::Handle); |
| bool maybe_truncated = |
| (value_view.size() == |
| std::min(static_cast<size_t>(att::kMaxReadByTypeValueLength), |
| mtu_max_value_size)); |
| |
| attributes.push_back( |
| ReadByTypeValue{handle, value_view, maybe_truncated}); |
| |
| // Advance list view to next pair (or end of list). |
| attr_list_view = attr_list_view.view(pair_size); |
| } |
| BT_ASSERT(attr_list_view.size() == 0); |
| |
| callback(fit::ok(std::move(attributes))); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void ReadBlobRequest(att::Handle handle, |
| uint16_t offset, |
| ReadCallback callback) override { |
| auto pdu = NewPDU(sizeof(att::ReadBlobRequestParams)); |
| if (!pdu) { |
| callback(ToResult(HostError::kOutOfMemory), |
| BufferView(), |
| /*maybe_truncated=*/false); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kReadBlobRequest, pdu.get()); |
| auto params = writer.mutable_payload<att::ReadBlobRequestParams>(); |
| params->handle = htole16(handle); |
| params->offset = htole16(offset); |
| |
| auto rsp_cb = [this, offset, callback = std::move(callback)]( |
| att::Bearer::TransactionResult result) { |
| if (result.is_ok()) { |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kReadBlobResponse); |
| bool maybe_truncated = |
| (static_cast<size_t>(offset) + rsp.payload_size() != |
| att::kMaxAttributeValueLength) && |
| (rsp.payload_data().size() == (mtu() - sizeof(att::OpCode))); |
| callback(fit::ok(), rsp.payload_data(), maybe_truncated); |
| return; |
| } |
| const auto& [error, handle] = result.error_value(); |
| bt_log(DEBUG, |
| "gatt", |
| "read blob request failed: %s, handle: %#.4x", |
| bt_str(error), |
| handle); |
| callback(fit::error(error), BufferView(), /*maybe_truncated=*/false); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void WriteRequest(att::Handle handle, |
| const ByteBuffer& value, |
| att::ResultFunction<> callback) override { |
| const size_t payload_size = sizeof(att::WriteRequestParams) + value.size(); |
| if (sizeof(att::OpCode) + payload_size > att_->mtu()) { |
| bt_log(TRACE, "gatt", "write request payload exceeds MTU"); |
| callback(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| auto pdu = NewPDU(payload_size); |
| if (!pdu) { |
| callback(ToResult(HostError::kOutOfMemory)); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kWriteRequest, pdu.get()); |
| auto params = writer.mutable_payload<att::WriteRequestParams>(); |
| params->handle = htole16(handle); |
| |
| auto value_view = |
| writer.mutable_payload_data().mutable_view(sizeof(att::Handle)); |
| value.Copy(&value_view); |
| |
| auto rsp_cb = [this, callback = std::move(callback)]( |
| att::Bearer::TransactionResult result) { |
| if (result.is_error()) { |
| const auto& [error, handle] = result.error_value(); |
| bt_log(DEBUG, |
| "gatt", |
| "write request failed: %s, handle: %#.2x", |
| bt_str(error), |
| handle); |
| callback(fit::error(error)); |
| return; |
| } |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kWriteResponse); |
| |
| if (rsp.payload_size()) { |
| att_->ShutDown(); |
| callback(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| callback(fit::ok()); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| // An internal object for storing the write queue, callback, and reliability |
| // mode of a long write operation. |
| struct PreparedWrite { |
| bt::att::PrepareWriteQueue prep_write_queue; |
| bt::att::ResultFunction<> callback; |
| ReliableMode reliable_mode; |
| }; |
| |
| void ExecutePrepareWrites(att::PrepareWriteQueue prep_write_queue, |
| ReliableMode reliable_mode, |
| att::ResultFunction<> callback) override { |
| PreparedWrite new_request; |
| new_request.prep_write_queue = std::move(prep_write_queue); |
| new_request.callback = std::move(callback); |
| new_request.reliable_mode = std::move(reliable_mode); |
| long_write_queue_.push(std::move(new_request)); |
| |
| // If the |long_write_queue| has a pending request, then appending this |
| // request will be sufficient, otherwise kick off the request. |
| if (long_write_queue_.size() == 1) { |
| ProcessWriteQueue(std::move(long_write_queue_.front())); |
| } |
| } |
| |
| void ProcessWriteQueue(PreparedWrite prep_write) { |
| if (!prep_write.prep_write_queue.empty()) { |
| att::QueuedWrite prep_write_request = |
| std::move(prep_write.prep_write_queue.front()); |
| // A copy of the |prep_write_request| is made to pass into the capture |
| // list for |prep_write_cb|. It will be used to validate the echoed blob. |
| auto prep_write_copy = att::QueuedWrite(prep_write_request.handle(), |
| prep_write_request.offset(), |
| prep_write_request.value()); |
| prep_write.prep_write_queue.pop(); |
| |
| auto prep_write_cb = [this, |
| prep_write = std::move(prep_write), |
| requested_blob = std::move(prep_write_copy)]( |
| att::Result<> status, |
| const ByteBuffer& blob) mutable { |
| // If the write fails, cancel the prep writes and then move on to the |
| // next long write in the queue. The device will echo the value written |
| // in the blob, according to the spec (Vol 3, Part G, 4.9.4). The offset |
| // and value will be verified if the requested |prep_write.mode| is |
| // enabled (Vol 3, Part G, 4.9.5). |
| |
| if (prep_write.reliable_mode == ReliableMode::kEnabled) { |
| if (blob.size() < sizeof(att::PrepareWriteResponseParams)) { |
| // The response blob is malformed. |
| status = ToResult(HostError::kNotReliable); |
| } else { |
| auto blob_offset = le16toh( |
| blob.ReadMember<&att::PrepareWriteResponseParams::offset>()); |
| auto blob_value = |
| blob.view(sizeof(att::PrepareWriteResponseParams)); |
| if ((blob_offset != requested_blob.offset()) || |
| !(blob_value == requested_blob.value())) { |
| status = ToResult(HostError::kNotReliable); |
| } |
| } |
| } |
| |
| if (status.is_error()) { |
| auto exec_write_cb = [this, |
| callback = std::move(prep_write.callback), |
| prep_write_status = |
| status](att::Result<> status) mutable { |
| // In this case return the original failure status. This effectively |
| // overrides the ExecuteWrite status. |
| callback(prep_write_status); |
| // Now that this request is complete, remove it from the overall |
| // queue. |
| BT_DEBUG_ASSERT(!long_write_queue_.empty()); |
| long_write_queue_.pop(); |
| |
| if (long_write_queue_.size() > 0) { |
| ProcessWriteQueue(std::move(long_write_queue_.front())); |
| } |
| }; |
| |
| ExecuteWriteRequest(att::ExecuteWriteFlag::kCancelAll, |
| std::move(exec_write_cb)); |
| |
| return; |
| } |
| |
| ProcessWriteQueue(std::move(prep_write)); |
| }; |
| |
| PrepareWriteRequest(prep_write_request.handle(), |
| prep_write_request.offset(), |
| std::move(prep_write_request.value()), |
| std::move(prep_write_cb)); |
| } |
| // End of this write, send and prepare for next item in overall write queue |
| else { |
| auto exec_write_cb = [this, callback = std::move(prep_write.callback)]( |
| att::Result<> status) mutable { |
| callback(status); |
| // Now that this request is complete, remove it from the overall |
| // queue. |
| BT_DEBUG_ASSERT(!long_write_queue_.empty()); |
| long_write_queue_.pop(); |
| |
| // If the super queue still has any long writes left to execute, |
| // initiate them |
| if (long_write_queue_.size() > 0) { |
| ProcessWriteQueue(std::move(long_write_queue_.front())); |
| } |
| }; |
| |
| ExecuteWriteRequest(att::ExecuteWriteFlag::kWritePending, |
| std::move(exec_write_cb)); |
| } |
| } |
| |
| void PrepareWriteRequest(att::Handle handle, |
| uint16_t offset, |
| const ByteBuffer& part_value, |
| PrepareCallback callback) override { |
| const size_t payload_size = |
| sizeof(att::PrepareWriteRequestParams) + part_value.size(); |
| if (sizeof(att::OpCode) + payload_size > att_->mtu()) { |
| bt_log(TRACE, "gatt", "prepare write request payload exceeds MTU"); |
| callback(ToResult(HostError::kPacketMalformed), BufferView()); |
| return; |
| } |
| |
| auto pdu = NewPDU(payload_size); |
| if (!pdu) { |
| callback(ToResult(HostError::kOutOfMemory), BufferView()); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kPrepareWriteRequest, pdu.get()); |
| auto params = writer.mutable_payload<att::PrepareWriteRequestParams>(); |
| params->handle = htole16(handle); |
| params->offset = htole16(offset); |
| |
| auto header_size = sizeof(att::Handle) + sizeof(uint16_t); |
| auto value_view = writer.mutable_payload_data().mutable_view(header_size); |
| part_value.Copy(&value_view); |
| |
| auto rsp_cb = [callback = std::move(callback)]( |
| att::Bearer::TransactionResult result) { |
| if (result.is_ok()) { |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kPrepareWriteResponse); |
| callback(fit::ok(), rsp.payload_data()); |
| return; |
| } |
| const auto& [error, handle] = result.error_value(); |
| bt_log(DEBUG, |
| "gatt", |
| "prepare write request failed: %s, handle:" |
| "%#.4x", |
| bt_str(error), |
| handle); |
| callback(fit::error(error), BufferView()); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void ExecuteWriteRequest(att::ExecuteWriteFlag flag, |
| att::ResultFunction<> callback) override { |
| const size_t payload_size = sizeof(att::ExecuteWriteRequestParams); |
| if (sizeof(att::OpCode) + payload_size > att_->mtu()) { |
| // This really shouldn't happen because we aren't consuming any actual |
| // payload here, but just in case... |
| bt_log(TRACE, "gatt", "execute write request size exceeds MTU"); |
| callback(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| auto pdu = NewPDU(payload_size); |
| if (!pdu) { |
| callback(ToResult(HostError::kOutOfMemory)); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kExecuteWriteRequest, pdu.get()); |
| auto params = writer.mutable_payload<att::ExecuteWriteRequestParams>(); |
| params->flags = flag; |
| |
| auto rsp_cb = [this, callback = std::move(callback)]( |
| att::Bearer::TransactionResult result) { |
| if (result.is_ok()) { |
| const att::PacketReader& rsp = result.value(); |
| BT_DEBUG_ASSERT(rsp.opcode() == att::kExecuteWriteResponse); |
| |
| if (rsp.payload_size()) { |
| att_->ShutDown(); |
| callback(ToResult(HostError::kPacketMalformed)); |
| return; |
| } |
| |
| callback(fit::ok()); |
| return; |
| } |
| const att::Error& error = result.error_value().first; |
| bt_log(DEBUG, "gatt", "execute write request failed: %s", bt_str(error)); |
| callback(fit::error(error)); |
| }; |
| |
| att_->StartTransaction(std::move(pdu), BindCallback(std::move(rsp_cb))); |
| } |
| |
| void WriteWithoutResponse(att::Handle handle, |
| const ByteBuffer& value, |
| att::ResultFunction<> callback) override { |
| const size_t payload_size = sizeof(att::WriteRequestParams) + value.size(); |
| if (sizeof(att::OpCode) + payload_size > att_->mtu()) { |
| bt_log(DEBUG, "gatt", "write request payload exceeds MTU"); |
| callback(ToResult(HostError::kFailed)); |
| return; |
| } |
| |
| auto pdu = NewPDU(payload_size); |
| if (!pdu) { |
| callback(ToResult(HostError::kOutOfMemory)); |
| return; |
| } |
| |
| att::PacketWriter writer(att::kWriteCommand, pdu.get()); |
| auto params = writer.mutable_payload<att::WriteRequestParams>(); |
| params->handle = htole16(handle); |
| |
| auto value_view = |
| writer.mutable_payload_data().mutable_view(sizeof(att::Handle)); |
| value.Copy(&value_view); |
| |
| [[maybe_unused]] bool _ = att_->SendWithoutResponse(std::move(pdu)); |
| callback(fit::ok()); |
| } |
| |
| void SetNotificationHandler(NotificationCallback handler) override { |
| notification_handler_ = std::move(handler); |
| } |
| |
| // Wraps |callback| in a TransactionCallback that only runs if this Client is |
| // still alive. |
| att::Bearer::TransactionCallback BindCallback( |
| att::Bearer::TransactionCallback callback) { |
| return [self = weak_self_.GetWeakPtr(), |
| callback = std::move(callback)](auto rsp) mutable { |
| if (self.is_alive()) { |
| callback(rsp); |
| } |
| }; |
| } |
| |
| att::Bearer::WeakPtr att_; |
| att::Bearer::HandlerId not_handler_id_; |
| att::Bearer::HandlerId ind_handler_id_; |
| |
| NotificationCallback notification_handler_; |
| // |long_write_queue_| contains long write requests, their |
| // associated callbacks and reliable write modes. |
| // Series of PrepareWrites are executed or cancelled at the same time so |
| // this is used to block while a single series is processed. |
| // |
| // While the top element is processed, the |PrepareWriteQueue| and callback |
| // will be empty and will be popped once the queue is cancelled or executed. |
| // Following the processing of each queue, the client will automatically |
| // process the next queue in the |long_write_queue_|. |
| std::queue<PreparedWrite> long_write_queue_; |
| WeakSelf<Client> weak_self_; |
| |
| BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Impl); |
| }; |
| |
| // static |
| std::unique_ptr<Client> Client::Create(att::Bearer::WeakPtr bearer) { |
| return std::make_unique<Impl>(std::move(bearer)); |
| } |
| |
| } // namespace bt::gatt |