blob: b39a4346092e5df79a3bc9de03b857d869cc507d [file] [log] [blame] [edit]
// 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 <cstdint>
#include "pw_bluetooth/att.emb.h"
#include "pw_bluetooth/hci_data.emb.h"
#include "pw_bluetooth/hci_h4.emb.h"
#include "pw_bluetooth/l2cap_frames.emb.h"
#include "pw_bluetooth_proxy/h4_packet.h"
#include "pw_bluetooth_proxy/l2cap_channel_common.h"
#include "pw_bluetooth_proxy/proxy_host.h"
#include "pw_bluetooth_proxy_private/test_utils.h"
#include "pw_function/function.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/try.h"
#include "pw_unit_test/framework.h"
#include "pw_unit_test/status_macros.h"
namespace pw::bluetooth::proxy {
namespace {
struct AttNotifyWithStorage {
BFrameWithStorage bframe;
emboss::AttHandleValueNtfWriter writer;
};
Result<AttNotifyWithStorage> SetupAttNotify(
uint16_t connection_handle,
uint16_t attribute_handle,
const pw::span<const uint8_t> attribute_value) {
const uint16_t kAttChannelId = 0x0004;
const size_t att_size =
emboss::AttHandleValueNtf::MinSizeInBytes() + attribute_value.size();
AttNotifyWithStorage frame;
PW_TRY_ASSIGN(frame.bframe,
SetupBFrame(connection_handle, kAttChannelId, att_size));
EXPECT_EQ(att_size, frame.bframe.writer.payload().SizeInBytes());
PW_TRY_ASSIGN(frame.writer,
MakeEmbossWriter<emboss::AttHandleValueNtfWriter>(
attribute_value.size(),
frame.bframe.writer.payload().BackingStorage().data(),
frame.bframe.writer.payload().SizeInBytes()));
EXPECT_TRUE(frame.writer.IsComplete());
frame.writer.attribute_opcode().Write(
emboss::AttOpcode::ATT_HANDLE_VALUE_NTF);
frame.writer.attribute_handle().Write(attribute_handle);
EXPECT_TRUE(
TryToCopyToEmbossStruct(frame.writer.attribute_value(), attribute_value));
return frame;
}
class GattNotifyTest : public ProxyHostTest {};
TEST_F(GattNotifyTest, TestSetupAttNotify) {
std::array<uint8_t, 1> attribute_value = {0xFA};
PW_TEST_ASSERT_OK_AND_ASSIGN(
AttNotifyWithStorage att,
SetupAttNotify(0x0ACB, 0x4321, pw::span(attribute_value)));
// Built from the preceding values in little endian order.
std::array<uint8_t, 13> expected_gatt_notify_packet = {0x02,
0xCB,
0x0A,
0x08,
0x00,
0x04,
0x00,
0x04,
0x00,
0x1B,
0x21,
0x43,
0xFA};
EXPECT_EQ(att.bframe.acl.h4_span().size(),
expected_gatt_notify_packet.size());
EXPECT_TRUE(std::equal(att.bframe.acl.h4_span().begin(),
att.bframe.acl.h4_span().end(),
expected_gatt_notify_packet.begin(),
expected_gatt_notify_packet.end()));
}
TEST_F(GattNotifyTest, GetAttributeHandle) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0);
GattNotifyChannel channel =
BuildGattNotifyChannel(proxy, {.attribute_handle = 0x234});
EXPECT_EQ(channel.attribute_handle(), 0x234);
}
TEST_F(GattNotifyTest, Send1ByteAttribute) {
struct {
int sends_called = 0;
// First four bits 0x0 encode PB & BC flags
uint16_t handle = 0x0ACB;
// Length of L2CAP PDU
uint16_t acl_data_total_length = 0x0008;
// Length of ATT PDU
uint16_t pdu_length = 0x0004;
// Attribute protocol channel ID (0x0004)
uint16_t channel_id = 0x0004;
// ATT_HANDLE_VALUE_NTF opcode 0x1B
uint8_t attribute_opcode = 0x1B;
uint16_t attribute_handle = 0x4321;
std::array<uint8_t, 1> attribute_value = {0xFA};
AttNotifyWithStorage att;
} capture;
PW_TEST_ASSERT_OK_AND_ASSIGN(
capture.att,
SetupAttNotify(/*connection_handle=*/capture.handle,
/*attribute_handle=*/capture.attribute_handle,
capture.attribute_value));
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
capture.sends_called++;
EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::ACL_DATA);
EXPECT_EQ(packet.GetH4Span().size(),
capture.att.bframe.acl.h4_span().size());
EXPECT_TRUE(std::equal(packet.GetH4Span().begin(),
packet.GetH4Span().end(),
capture.att.bframe.acl.h4_span().begin(),
capture.att.bframe.acl.h4_span().end()));
PW_TEST_ASSERT_OK_AND_ASSIGN(
auto acl,
MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan()));
emboss::BFrameView l2cap =
emboss::MakeBFrameView(acl.payload().BackingStorage().data(),
acl.data_total_length().Read());
emboss::AttHandleValueNtfView gatt_notify =
emboss::MakeAttHandleValueNtfView(
capture.attribute_value.size(),
l2cap.payload().BackingStorage().data(),
l2cap.pdu_length().Read());
EXPECT_EQ(acl.header().handle().Read(), capture.handle);
EXPECT_EQ(acl.header().packet_boundary_flag().Read(),
emboss::AclDataPacketBoundaryFlag::FIRST_NON_FLUSHABLE);
EXPECT_EQ(acl.header().broadcast_flag().Read(),
emboss::AclDataPacketBroadcastFlag::POINT_TO_POINT);
EXPECT_EQ(acl.data_total_length().Read(),
capture.acl_data_total_length);
EXPECT_EQ(l2cap.pdu_length().Read(), capture.pdu_length);
EXPECT_EQ(l2cap.channel_id().Read(), capture.channel_id);
EXPECT_EQ(gatt_notify.attribute_opcode().Read(),
static_cast<emboss::AttOpcode>(capture.attribute_opcode));
EXPECT_EQ(gatt_notify.attribute_handle().Read(),
capture.attribute_handle);
EXPECT_EQ(gatt_notify.attribute_value()[0].Read(),
capture.attribute_value[0]);
});
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/1,
/*br_edr_acl_credits_to_reserve=*/0);
// Allow proxy to reserve 1 credit.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
GattNotifyChannel channel = BuildGattNotifyChannel(
proxy,
{.handle = capture.handle, .attribute_handle = capture.attribute_handle});
PW_TEST_EXPECT_OK(
channel.Write(MultiBufFromArray(capture.attribute_value)).status);
EXPECT_EQ(capture.sends_called, 1);
}
TEST_F(GattNotifyTest, Send2ByteAttribute) {
struct {
int sends_called = 0;
// Max connection_handle value; first four bits 0x0 encode PB & BC flags
const uint16_t handle = 0x0EFF;
// Length of L2CAP PDU
const uint16_t acl_data_total_length = 0x0009;
// Length of ATT PDU
const uint16_t pdu_length = 0x0005;
// Attribute protocol channel ID (0x0004)
const uint16_t channel_id = 0x0004;
// ATT_HANDLE_VALUE_NTF opcode 0x1B
const uint8_t attribute_opcode = 0x1B;
const uint16_t attribute_handle = 0x1234;
const std::array<uint8_t, 2> attribute_value = {0xAB, 0xCD};
AttNotifyWithStorage att;
} capture;
PW_TEST_ASSERT_OK_AND_ASSIGN(
capture.att,
SetupAttNotify(/*connection_handle=*/capture.handle,
/*attribute_handle=*/capture.attribute_handle,
capture.attribute_value));
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
++capture.sends_called;
EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::ACL_DATA);
EXPECT_EQ(packet.GetH4Span().size(),
capture.att.bframe.acl.h4_span().size());
EXPECT_TRUE(std::equal(packet.GetH4Span().begin(),
packet.GetH4Span().end(),
capture.att.bframe.acl.h4_span().begin(),
capture.att.bframe.acl.h4_span().end()));
PW_TEST_ASSERT_OK_AND_ASSIGN(
auto acl,
MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan()));
emboss::BFrameView l2cap = emboss::MakeBFrameView(
acl.payload().BackingStorage().data(), acl.SizeInBytes());
emboss::AttHandleValueNtfView gatt_notify =
emboss::MakeAttHandleValueNtfView(
capture.attribute_value.size(),
l2cap.payload().BackingStorage().data(),
l2cap.pdu_length().Read());
EXPECT_EQ(acl.header().handle().Read(), capture.handle);
EXPECT_EQ(acl.header().packet_boundary_flag().Read(),
emboss::AclDataPacketBoundaryFlag::FIRST_NON_FLUSHABLE);
EXPECT_EQ(acl.header().broadcast_flag().Read(),
emboss::AclDataPacketBroadcastFlag::POINT_TO_POINT);
EXPECT_EQ(acl.data_total_length().Read(),
capture.acl_data_total_length);
EXPECT_EQ(l2cap.pdu_length().Read(), capture.pdu_length);
EXPECT_EQ(l2cap.channel_id().Read(), capture.channel_id);
EXPECT_EQ(gatt_notify.attribute_opcode().Read(),
static_cast<emboss::AttOpcode>(capture.attribute_opcode));
EXPECT_EQ(gatt_notify.attribute_handle().Read(),
capture.attribute_handle);
EXPECT_EQ(gatt_notify.attribute_value()[0].Read(),
capture.attribute_value[0]);
EXPECT_EQ(gatt_notify.attribute_value()[1].Read(),
capture.attribute_value[1]);
});
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/1,
/*br_edr_acl_credits_to_reserve=*/0);
// Allow proxy to reserve 1 credit.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
GattNotifyChannel channel = BuildGattNotifyChannel(
proxy,
{.handle = capture.handle, .attribute_handle = capture.attribute_handle});
PW_TEST_EXPECT_OK(
channel.Write(MultiBufFromArray(capture.attribute_value)).status);
EXPECT_EQ(capture.sends_called, 1);
}
TEST_F(GattNotifyTest, ReturnsErrorIfAttributeTooLarge) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4 packet) { FAIL(); });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 0));
// attribute_value 1 byte too large
std::array<uint8_t,
proxy.GetMaxAclSendSize() -
emboss::AclDataFrameHeader::IntrinsicSizeInBytes() -
emboss::BasicL2capHeader::IntrinsicSizeInBytes() -
emboss::AttHandleValueNtf::MinSizeInBytes() + 1>
attribute_value_too_large;
GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {});
EXPECT_EQ(channel.Write(MultiBufFromArray(attribute_value_too_large)).status,
PW_STATUS_INVALID_ARGUMENT);
}
TEST_F(GattNotifyTest, ChannelIsNotConstructedIfParametersInvalid) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4 packet) { FAIL(); });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0);
// attribute value is zero
EXPECT_EQ(
BuildGattNotifyChannelWithResult(proxy, {.attribute_handle = 0}).status(),
PW_STATUS_INVALID_ARGUMENT);
// connection_handle too large
EXPECT_EQ(
BuildGattNotifyChannelWithResult(proxy, {.handle = 0x0FFF}).status(),
PW_STATUS_INVALID_ARGUMENT);
}
TEST_F(GattNotifyTest, PayloadIsReturnedOnError) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4 packet) { FAIL(); });
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0);
const std::array<const uint8_t, 2> attribute_value = {5};
GattNotifyChannel channel = BuildGattNotifyChannel(proxy, {});
StatusWithMultiBuf result =
channel.Write(MultiBufFromSpan(pw::span{attribute_value}));
ASSERT_EQ(result.status, Status::FailedPrecondition());
auto s = result.buf->ContiguousSpan();
ASSERT_TRUE(s.has_value());
EXPECT_EQ(s.value().size(), attribute_value.size());
EXPECT_EQ((std::byte)attribute_value[0], s.value().data()[0]);
}
} // namespace
} // namespace pw::bluetooth::proxy