diff --git a/pw_bluetooth_sapphire/BUILD.bazel b/pw_bluetooth_sapphire/BUILD.bazel index 5313c789f9..1a4d682501 100644 --- a/pw_bluetooth_sapphire/BUILD.bazel +++ b/pw_bluetooth_sapphire/BUILD.bazel @@ -180,6 +180,7 @@ cc_library( "public/pw_bluetooth_sapphire/internal/host/l2cap/fragmenter.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/frame_headers.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h", + "public/pw_bluetooth_sapphire/internal/host/l2cap/le_dynamic_channel.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/le_signaling_channel.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/logical_link.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h", diff --git a/pw_bluetooth_sapphire/BUILD.gn b/pw_bluetooth_sapphire/BUILD.gn index 24b9f2b2ed..96ca794668 100644 --- a/pw_bluetooth_sapphire/BUILD.gn +++ b/pw_bluetooth_sapphire/BUILD.gn @@ -215,6 +215,7 @@ pw_source_set("_public") { "public/pw_bluetooth_sapphire/internal/host/l2cap/fragmenter.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/frame_headers.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h", + "public/pw_bluetooth_sapphire/internal/host/l2cap/le_dynamic_channel.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/le_signaling_channel.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/logical_link.h", "public/pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h", diff --git a/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel b/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel index bc25666e65..d26c269cc3 100644 --- a/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel +++ b/pw_bluetooth_sapphire/host/l2cap/BUILD.bazel @@ -50,6 +50,7 @@ cc_library( "enhanced_retransmission_mode_tx_engine.cc", "fcs.cc", "fragmenter.cc", + "le_dynamic_channel.cc", "le_signaling_channel.cc", "logical_link.cc", "low_energy_command_handler.cc", @@ -139,6 +140,7 @@ pw_cc_test( "fcs_test.cc", "fragmenter_test.cc", "frame_headers_test.cc", + "le_dynamic_channel_test.cc", "le_signaling_channel_test.cc", "logical_link_test.cc", "low_energy_command_handler_test.cc", diff --git a/pw_bluetooth_sapphire/host/l2cap/BUILD.gn b/pw_bluetooth_sapphire/host/l2cap/BUILD.gn index fcda24eb07..acfb88444a 100644 --- a/pw_bluetooth_sapphire/host/l2cap/BUILD.gn +++ b/pw_bluetooth_sapphire/host/l2cap/BUILD.gn @@ -53,6 +53,7 @@ pw_source_set("l2cap") { "$dir_public_l2cap/enhanced_retransmission_mode_tx_engine.h", "$dir_public_l2cap/fcs.h", "$dir_public_l2cap/fragmenter.h", + "$dir_public_l2cap/le_dynamic_channel.h", "$dir_public_l2cap/le_signaling_channel.h", "$dir_public_l2cap/logical_link.h", "$dir_public_l2cap/low_energy_command_handler.h", @@ -85,6 +86,7 @@ pw_source_set("l2cap") { "enhanced_retransmission_mode_tx_engine.cc", "fcs.cc", "fragmenter.cc", + "le_dynamic_channel.cc", "le_signaling_channel.cc", "logical_link.cc", "low_energy_command_handler.cc", @@ -191,6 +193,7 @@ pw_test("l2cap_tests") { "fcs_test.cc", "fragmenter_test.cc", "frame_headers_test.cc", + "le_dynamic_channel_test.cc", "le_signaling_channel_test.cc", "logical_link_test.cc", "low_energy_command_handler_test.cc", diff --git a/pw_bluetooth_sapphire/host/l2cap/channel.cc b/pw_bluetooth_sapphire/host/l2cap/channel.cc index a1f3093c6e..b7bb833e6d 100644 --- a/pw_bluetooth_sapphire/host/l2cap/channel.cc +++ b/pw_bluetooth_sapphire/host/l2cap/channel.cc @@ -25,6 +25,8 @@ #include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/basic_mode_rx_engine.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/basic_mode_tx_engine.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/credit_based_flow_control_rx_engine.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/credit_based_flow_control_tx_engine.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/enhanced_retransmission_mode_engines.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/logical_link.h" @@ -135,22 +137,35 @@ ChannelImpl::ChannelImpl(pw::async::Dispatcher& dispatcher, BT_ASSERT_MSG( info_.mode == RetransmissionAndFlowControlMode::kBasic || info_.mode == - RetransmissionAndFlowControlMode::kEnhancedRetransmission, + RetransmissionAndFlowControlMode::kEnhancedRetransmission || + info.mode == CreditBasedFlowControlMode::kLeCreditBasedFlowControl, "Channel constructed with unsupported mode: %s\n", AnyChannelModeToString(info_.mode).c_str()); + auto connection_failure_cb = [link] { + if (link.is_alive()) { + // |link| is expected to ignore this call if it has been closed. + link->SignalError(); + } + }; + if (info_.mode == RetransmissionAndFlowControlMode::kBasic) { rx_engine_ = std::make_unique(); tx_engine_ = std::make_unique(id, max_tx_sdu_size(), *this); + } else if (std::holds_alternative(info_.mode)) { + BT_ASSERT(info_.remote_initial_credits.has_value()); + auto mode = std::get(info_.mode); + rx_engine_ = std::make_unique( + std::move(connection_failure_cb)); + tx_engine_ = std::make_unique( + id, + max_tx_sdu_size(), + *this, + mode, + info_.max_tx_pdu_payload_size, + *info_.remote_initial_credits); } else { - // Must capture |link| and not |link_| to avoid having to take |mutex_|. - auto connection_failure_cb = [link] { - if (link.is_alive()) { - // |link| is expected to ignore this call if it has been closed. - link->SignalError(); - } - }; std::tie(rx_engine_, tx_engine_) = MakeLinkedEnhancedRetransmissionModeEngines( id, diff --git a/pw_bluetooth_sapphire/host/l2cap/le_dynamic_channel.cc b/pw_bluetooth_sapphire/host/l2cap/le_dynamic_channel.cc new file mode 100644 index 0000000000..5d56f2a03f --- /dev/null +++ b/pw_bluetooth_sapphire/host/l2cap/le_dynamic_channel.cc @@ -0,0 +1,303 @@ +// Copyright 2024 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/l2cap/le_dynamic_channel.h" + +#include + +#include + +#include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/types.h" + +namespace bt::l2cap::internal { +namespace { + +constexpr uint16_t kLeDynamicChannelCount = + kLastLEDynamicChannelId - kFirstDynamicChannelId + 1; + +CreditBasedFlowControlMode ConvertMode(AnyChannelMode mode) { + // LE dynamic channels only support credit-based flow control modes. + BT_ASSERT(std::holds_alternative(mode)); + return std::get(mode); +} + +} // namespace + +LeDynamicChannelRegistry::LeDynamicChannelRegistry( + SignalingChannelInterface* sig, + DynamicChannelCallback close_cb, + ServiceRequestCallback service_request_cb, + bool random_channel_ids) + : DynamicChannelRegistry(kLeDynamicChannelCount, + std::move(close_cb), + std::move(service_request_cb), + random_channel_ids), + sig_(sig) { + BT_DEBUG_ASSERT(sig_); +} + +DynamicChannelPtr LeDynamicChannelRegistry::MakeOutbound( + Psm psm, ChannelId local_cid, ChannelParameters params) { + return LeDynamicChannel::MakeOutbound(this, sig_, psm, local_cid, params); +} + +DynamicChannelPtr LeDynamicChannelRegistry::MakeInbound( + [[maybe_unused]] Psm psm, + [[maybe_unused]] ChannelId local_cid, + [[maybe_unused]] ChannelId remote_cid, + [[maybe_unused]] ChannelParameters params) { + // Not yet implemented. + return nullptr; +} + +std::unique_ptr LeDynamicChannel::MakeOutbound( + DynamicChannelRegistry* registry, + SignalingChannelInterface* signaling_channel, + Psm psm, + ChannelId local_cid, + ChannelParameters params) { + return std::unique_ptr(new LeDynamicChannel( + registry, signaling_channel, psm, local_cid, kInvalidChannelId, params)); +} + +std::string LeDynamicChannel::State::ToString() const { + return std::string("{exchanged_connection_request: ") + + (exchanged_connection_request ? "true" : "false") + + ", exchanged_connection_response: " + + (exchanged_connection_response ? "true" : "false") + + ", exchanged_disconnect_request: " + + (exchanged_disconnect_request ? "true" : "false") + "}"; +} + +LeDynamicChannel::LeDynamicChannel(DynamicChannelRegistry* registry, + SignalingChannelInterface* signaling_channel, + Psm psm, + ChannelId local_cid, + ChannelId remote_cid, + ChannelParameters params) + : DynamicChannel(registry, psm, local_cid, remote_cid), + signaling_channel_(signaling_channel), + flow_control_mode_(ConvertMode(params.mode.value_or( + CreditBasedFlowControlMode::kLeCreditBasedFlowControl))), + state_(), + local_config_( + LeChannelConfig{.mtu = params.max_rx_sdu_size.value_or(kDefaultMTU), + .mps = kMaxInboundPduPayloadSize}), + remote_config_(std::nullopt), + weak_self_(this) {} + +void LeDynamicChannel::TriggerOpenCallback() { + auto cb = std::move(open_result_cb_); + if (cb) { + cb(); + } +} + +void LeDynamicChannel::Open(fit::closure open_cb) { + BT_ASSERT_MSG(!open_result_cb_, "open callback already set"); + open_result_cb_ = std::move(open_cb); + if (state_.exchanged_connection_request) { + TriggerOpenCallback(); + return; + } + + auto on_conn_rsp = + [self = weak_self_.GetWeakPtr()]( + const LowEnergyCommandHandler::LeCreditBasedConnectionResponse& + rsp) mutable { + if (self.is_alive()) { + self->OnRxLeCreditConnRsp(rsp); + self->TriggerOpenCallback(); + } + }; + + auto on_conn_rsp_timeout = [cid = local_cid()] { + bt_log(WARN, + "l2cap-le", + "Channel %#.4x: Timed out waiting for Connection Response", + cid); + }; + + LowEnergyCommandHandler cmd_handler(signaling_channel_, + std::move(on_conn_rsp_timeout)); + if (!cmd_handler.SendLeCreditBasedConnectionRequest(psm(), + local_cid(), + local_config_.mtu, + local_config_.mps, + 0, + on_conn_rsp)) { + bt_log(ERROR, + "l2cap-le", + "Channel %#.4x: Failed to send Connection Request", + local_cid()); + TriggerOpenCallback(); + return; + } + + state_.exchanged_connection_request = true; +} + +void LeDynamicChannel::Disconnect(DisconnectDoneCallback done_cb) { + BT_ASSERT(done_cb); + if (!IsConnected()) { + done_cb(); + return; + } + + auto on_discon_rsp = + [local_cid = local_cid(), + remote_cid = remote_cid(), + self = weak_self_.GetWeakPtr(), + done_cb = done_cb.share()]( + const LowEnergyCommandHandler::DisconnectionResponse& rsp) mutable { + if (rsp.local_cid() != local_cid || rsp.remote_cid() != remote_cid) { + bt_log(WARN, + "l2cap-le", + "Channel %#.4x: Got Disconnection Response with ID %#.4x/" + "remote ID %#.4x on channel with remote ID %#.4x", + local_cid, + rsp.local_cid(), + rsp.remote_cid(), + remote_cid); + } else { + bt_log(TRACE, + "l2cap-le", + "Channel %#.4x: Got Disconnection Response", + local_cid); + } + + if (self.is_alive()) { + done_cb(); + } + }; + + auto on_discon_rsp_timeout = [local_cid = local_cid(), + self = weak_self_.GetWeakPtr(), + done_cb = done_cb.share()]() mutable { + bt_log(WARN, + "l2cap-le", + "Channel %#.4x: Timed out waiting for Disconnection Response; " + "completing disconnection", + local_cid); + if (self.is_alive()) { + done_cb(); + } + }; + + LowEnergyCommandHandler cmd_handler(signaling_channel_, + std::move(on_discon_rsp_timeout)); + if (!cmd_handler.SendDisconnectionRequest( + remote_cid(), local_cid(), std::move(on_discon_rsp))) { + bt_log(WARN, + "l2cap-le", + "Channel %#.4x: Failed to send Disconnection Request", + local_cid()); + done_cb(); + return; + } + + state_.exchanged_disconnect_request = true; + bt_log(TRACE, + "l2cap-le", + "Channel %#.4x: Sent Disconnection Request", + local_cid()); + return; +} + +bool LeDynamicChannel::IsConnected() const { + return state_.exchanged_connection_request && + state_.exchanged_connection_response && + !state_.exchanged_disconnect_request && + remote_cid() != kInvalidChannelId; +} + +bool LeDynamicChannel::IsOpen() const { + // Since dynamic LE L2CAP channels don't have channel configuration state + // machines, `IsOpen` and `IsConnected` are equivalent. + return IsConnected(); +} + +ChannelInfo LeDynamicChannel::info() const { + BT_ASSERT(remote_config_.has_value()); + return ChannelInfo::MakeCreditBasedFlowControlMode( + flow_control_mode_, + local_config_.mtu, + remote_config_->mtu, + remote_config_->mps, + remote_config_->initial_credits); +} + +void LeDynamicChannel::OnRxLeCreditConnRsp( + const LowEnergyCommandHandler::LeCreditBasedConnectionResponse& rsp) { + if (state_.exchanged_connection_response || + !state_.exchanged_connection_request) { + bt_log(ERROR, + "l2cap-le", + "Channel %#.4x: Unexpected Connection Response, state %s", + local_cid(), + bt_str(state_)); + return; + } + + if (rsp.status() == LowEnergyCommandHandler::Status::kReject) { + bt_log(ERROR, + "l2cap-le", + "Channel %#.4x: Connection Request rejected reason %#.4hx", + local_cid(), + static_cast(rsp.reject_reason())); + return; + } + + if (rsp.result() != LECreditBasedConnectionResult::kSuccess) { + bt_log(ERROR, + "l2cap-le", + "Channel %#.4x: Connection request failed, result %#.4hx", + local_cid(), + static_cast(rsp.result())); + return; + } + + if (rsp.destination_cid() < kFirstDynamicChannelId) { + bt_log(ERROR, + "l2cap-le", + "Channel %#.4x: Remote channel ID is invalid.", + local_cid()); + return; + } + + if (!SetRemoteChannelId(rsp.destination_cid())) { + bt_log(ERROR, + "l2cap-le", + "Channel %#.4x: Remote channel ID %#.4x is not unique", + local_cid(), + rsp.destination_cid()); + return; + } + + bt_log(TRACE, + "l2cap-le", + "Channel %#.4x: Got remote channel ID %#.4x", + local_cid(), + remote_cid()); + + remote_config_ = LeChannelConfig{.mtu = rsp.mtu(), + .mps = rsp.mps(), + .initial_credits = rsp.initial_credits()}; + state_.exchanged_connection_response = true; + set_opened(); +} + +} // namespace bt::l2cap::internal diff --git a/pw_bluetooth_sapphire/host/l2cap/le_dynamic_channel_test.cc b/pw_bluetooth_sapphire/host/l2cap/le_dynamic_channel_test.cc new file mode 100644 index 0000000000..1c438cd4ec --- /dev/null +++ b/pw_bluetooth_sapphire/host/l2cap/le_dynamic_channel_test.cc @@ -0,0 +1,438 @@ +// Copyright 2024 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/l2cap/le_dynamic_channel.h" + +#include + +#include "pw_async/fake_dispatcher_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_signaling_channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/signaling_channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/types.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" +#include "pw_unit_test/framework.h" + +namespace bt::l2cap::internal { +namespace { +constexpr ChannelId DynamicCid(int16_t channel_number = 0) { + return kFirstDynamicChannelId + channel_number; +} + +auto LeConnReq(const LECreditBasedConnectionRequestPayload& payload) { + return StaticByteBuffer(LowerBits(payload.le_psm), + UpperBits(payload.le_psm), + LowerBits(payload.src_cid), + UpperBits(payload.src_cid), + LowerBits(payload.mtu), + UpperBits(payload.mtu), + LowerBits(payload.mps), + UpperBits(payload.mps), + LowerBits(payload.initial_credits), + UpperBits(payload.initial_credits)); +} + +auto LeConnRsp(const LECreditBasedConnectionResponsePayload& payload) { + return StaticByteBuffer(LowerBits(payload.dst_cid), + UpperBits(payload.dst_cid), + LowerBits(payload.mtu), + UpperBits(payload.mtu), + LowerBits(payload.mps), + UpperBits(payload.mps), + LowerBits(payload.initial_credits), + UpperBits(payload.initial_credits), + LowerBits(static_cast(payload.result)), + UpperBits(static_cast(payload.result))); +} + +auto DisconnReq(const DisconnectionRequestPayload& payload) { + return StaticByteBuffer(LowerBits(payload.dst_cid), + UpperBits(payload.dst_cid), + LowerBits(payload.src_cid), + UpperBits(payload.src_cid)); +} + +auto DisconnRsp(const DisconnectionResponsePayload& payload) { + return StaticByteBuffer(LowerBits(payload.dst_cid), + UpperBits(payload.dst_cid), + LowerBits(payload.src_cid), + UpperBits(payload.src_cid)); +} + +class LeDynamicChannelTest : public pw::async::test::FakeDispatcherFixture { + public: + LeDynamicChannelTest() = default; + ~LeDynamicChannelTest() override = default; + + protected: + // Import types for brevity. + using DynamicChannelCallback = DynamicChannelRegistry::DynamicChannelCallback; + using ServiceRequestCallback = DynamicChannelRegistry::ServiceRequestCallback; + + // TestLoopFixture overrides + void SetUp() override { + channel_close_cb_ = nullptr; + service_request_cb_ = nullptr; + signaling_channel_ = + std::make_unique(dispatcher()); + + registry_ = std::make_unique( + sig(), + fit::bind_member<&LeDynamicChannelTest::OnChannelClose>(this), + fit::bind_member<&LeDynamicChannelTest::OnServiceRequest>(this), + /*random_channel_ids=*/false); + } + + void TearDown() override { + RunUntilIdle(); + registry_ = nullptr; + signaling_channel_ = nullptr; + service_request_cb_ = nullptr; + channel_close_cb_ = nullptr; + } + + const DynamicChannel* DoOpenOutbound( + const LECreditBasedConnectionRequestPayload& request, + const LECreditBasedConnectionResponsePayload& response, + ChannelParameters params, + SignalingChannel::Status response_status = + SignalingChannel::Status::kSuccess) { + auto req = LeConnReq(request); + auto rsp = LeConnRsp(response); + EXPECT_OUTBOUND_REQ(*sig(), + kLECreditBasedConnectionRequest, + req.view(), + {response_status, rsp.view()}); + + auto channel = std::make_shared>(); + registry()->OpenOutbound( + request.le_psm, params, [weak = std::weak_ptr(channel)](auto chan) { + if (auto channel = weak.lock()) { + *channel = chan; + } + }); + + RunUntilIdle(); + if (HasFatalFailure()) { + return nullptr; + } + + // We should always get a result, whether it is a success or failure. + EXPECT_TRUE(*channel); + if (!*channel) { + return nullptr; + } + + return **channel; + } + + bool DoCloseOutbound(const DisconnectionRequestPayload& request, + const DisconnectionResponsePayload& response) { + auto req = DisconnReq(request); + auto rsp = DisconnRsp(response); + EXPECT_OUTBOUND_REQ(*sig(), + kDisconnectionRequest, + req.view(), + {SignalingChannel::Status::kSuccess, rsp.view()}); + bool channel_close_cb_called = false; + registry()->CloseChannel(request.src_cid, + [&] { channel_close_cb_called = true; }); + RunUntilIdle(); + if (HasFatalFailure()) { + return false; + } + + return channel_close_cb_called; + } + + bool DoCloseOutbound(const DisconnectionRequestPayload& request) { + DisconnectionResponsePayload response = {request.dst_cid, request.src_cid}; + return DoCloseOutbound(request, response); + } + + testing::FakeSignalingChannel* sig() const { + return signaling_channel_.get(); + } + + LeDynamicChannelRegistry* registry() const { return registry_.get(); } + + void set_channel_close_cb(DynamicChannelCallback close_cb) { + channel_close_cb_ = std::move(close_cb); + } + + void set_service_request_cb(ServiceRequestCallback service_request_cb) { + service_request_cb_ = std::move(service_request_cb); + } + + private: + void OnChannelClose(const DynamicChannel* channel) { + if (channel_close_cb_) { + channel_close_cb_(channel); + } + } + + std::optional OnServiceRequest(Psm psm) { + if (service_request_cb_) { + return service_request_cb_(psm); + } + return std::nullopt; + } + + DynamicChannelCallback channel_close_cb_; + ServiceRequestCallback service_request_cb_; + std::unique_ptr signaling_channel_; + std::unique_ptr registry_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LeDynamicChannelTest); +}; + +TEST_F(LeDynamicChannelTest, StateToString) { + constexpr std::array, 4> + kCases{{ + {{.exchanged_connection_request = false, + .exchanged_connection_response = false, + .exchanged_disconnect_request = false}, + "{exchanged_connection_request: false, " + "exchanged_connection_response: false, " + "exchanged_disconnect_request: false}"}, + {{.exchanged_connection_request = false, + .exchanged_connection_response = false, + .exchanged_disconnect_request = true}, + "{exchanged_connection_request: false, " + "exchanged_connection_response: false, " + "exchanged_disconnect_request: true}"}, + {{.exchanged_connection_request = false, + .exchanged_connection_response = true, + .exchanged_disconnect_request = false}, + "{exchanged_connection_request: false, " + "exchanged_connection_response: true, " + "exchanged_disconnect_request: false}"}, + {{.exchanged_connection_request = true, + .exchanged_connection_response = false, + .exchanged_disconnect_request = false}, + "{exchanged_connection_request: true, " + "exchanged_connection_response: false, " + "exchanged_disconnect_request: false}"}, + }}; + for (const auto& [state, expected] : kCases) { + EXPECT_EQ(state.ToString(), expected); + } +} + +TEST_F(LeDynamicChannelTest, OpenOutboundDefaultParametersCloseOutbound) { + static constexpr auto kMode = + CreditBasedFlowControlMode::kLeCreditBasedFlowControl; + static constexpr ChannelParameters kChannelParams{ + .mode = kMode, + .max_rx_sdu_size = std::nullopt, + .flush_timeout = std::nullopt, + }; + static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ + .le_psm = 0x0015, + .src_cid = DynamicCid(), + .mtu = kDefaultMTU, + .mps = kMaxInboundPduPayloadSize, + .initial_credits = 0x0000, + }; + static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ + .dst_cid = DynamicCid(), + .mtu = 0x0064, + .mps = 0x0032, + .initial_credits = 0x0050, + .result = LECreditBasedConnectionResult::kSuccess, + }; + static constexpr DisconnectionRequestPayload kDisconnReqPayload{ + .dst_cid = kLeConnRspPayload.dst_cid, + .src_cid = kLeConnReqPayload.src_cid, + }; + + auto chan = + DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); + ASSERT_TRUE(chan); + EXPECT_TRUE(chan->IsOpen()); + EXPECT_TRUE(chan->IsConnected()); + EXPECT_EQ(kLeConnReqPayload.src_cid, chan->local_cid()); + EXPECT_EQ(kLeConnRspPayload.dst_cid, chan->remote_cid()); + + ChannelInfo expected_info = ChannelInfo::MakeCreditBasedFlowControlMode( + kMode, + kDefaultMTU, + kLeConnRspPayload.mtu, + kLeConnRspPayload.mps, + kLeConnRspPayload.initial_credits); + ChannelInfo actual_info = chan->info(); + + EXPECT_EQ(expected_info.mode, actual_info.mode); + EXPECT_EQ(expected_info.max_rx_sdu_size, actual_info.max_rx_sdu_size); + EXPECT_EQ(expected_info.max_tx_sdu_size, actual_info.max_tx_sdu_size); + EXPECT_EQ(expected_info.n_frames_in_tx_window, + actual_info.n_frames_in_tx_window); + EXPECT_EQ(expected_info.max_transmissions, actual_info.max_transmissions); + EXPECT_EQ(expected_info.max_tx_pdu_payload_size, + actual_info.max_tx_pdu_payload_size); + EXPECT_EQ(expected_info.psm, actual_info.psm); + EXPECT_EQ(expected_info.flush_timeout, actual_info.flush_timeout); + EXPECT_EQ(expected_info.remote_initial_credits, + actual_info.remote_initial_credits); + + EXPECT_TRUE(DoCloseOutbound(kDisconnReqPayload)); +} + +TEST_F(LeDynamicChannelTest, OpenOutboundSpecificParametersCloseOutbound) { + static constexpr auto kMode = + CreditBasedFlowControlMode::kLeCreditBasedFlowControl; + static constexpr ChannelParameters kChannelParams{ + .mode = kMode, + .max_rx_sdu_size = 0x0023, + .flush_timeout = std::nullopt, + }; + static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ + .le_psm = 0x0015, + .src_cid = DynamicCid(), + .mtu = kChannelParams.max_rx_sdu_size.value_or(0), + .mps = kMaxInboundPduPayloadSize, + .initial_credits = 0x0000, + }; + static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ + .dst_cid = DynamicCid(), + .mtu = 0x0064, + .mps = 0x0032, + .initial_credits = 0x0050, + .result = LECreditBasedConnectionResult::kSuccess, + }; + static constexpr DisconnectionRequestPayload kDisconnReqPayload{ + .dst_cid = kLeConnRspPayload.dst_cid, + .src_cid = kLeConnReqPayload.src_cid, + }; + + auto chan = + DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); + ASSERT_TRUE(chan); + EXPECT_TRUE(chan->IsOpen()); + EXPECT_TRUE(chan->IsConnected()); + EXPECT_EQ(kLeConnReqPayload.src_cid, chan->local_cid()); + EXPECT_EQ(kLeConnRspPayload.dst_cid, chan->remote_cid()); + + ChannelInfo expected_info = ChannelInfo::MakeCreditBasedFlowControlMode( + kMode, + *kChannelParams.max_rx_sdu_size, + kLeConnRspPayload.mtu, + kLeConnRspPayload.mps, + kLeConnRspPayload.initial_credits); + ChannelInfo actual_info = chan->info(); + + EXPECT_EQ(expected_info.mode, actual_info.mode); + EXPECT_EQ(expected_info.max_rx_sdu_size, actual_info.max_rx_sdu_size); + EXPECT_EQ(expected_info.max_tx_sdu_size, actual_info.max_tx_sdu_size); + EXPECT_EQ(expected_info.n_frames_in_tx_window, + actual_info.n_frames_in_tx_window); + EXPECT_EQ(expected_info.max_transmissions, actual_info.max_transmissions); + EXPECT_EQ(expected_info.max_tx_pdu_payload_size, + actual_info.max_tx_pdu_payload_size); + EXPECT_EQ(expected_info.psm, actual_info.psm); + EXPECT_EQ(expected_info.flush_timeout, actual_info.flush_timeout); + EXPECT_EQ(expected_info.remote_initial_credits, + actual_info.remote_initial_credits); + + EXPECT_TRUE(DoCloseOutbound(kDisconnReqPayload)); +} + +TEST_F(LeDynamicChannelTest, OpenOutboundBadChannel) { + static constexpr auto kMode = + CreditBasedFlowControlMode::kLeCreditBasedFlowControl; + static constexpr ChannelParameters kChannelParams{ + .mode = kMode, + .max_rx_sdu_size = std::nullopt, + .flush_timeout = std::nullopt, + }; + static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ + .le_psm = 0x0015, + .src_cid = DynamicCid(), + .mtu = kDefaultMTU, + .mps = kMaxInboundPduPayloadSize, + .initial_credits = 0x0000, + }; + static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ + .dst_cid = DynamicCid(-1), + .mtu = 0x0064, + .mps = 0x0032, + .initial_credits = 0x0050, + .result = LECreditBasedConnectionResult::kSuccess, + }; + + auto chan = + DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); + EXPECT_FALSE(chan); +} + +TEST_F(LeDynamicChannelTest, OpenOutboundRejected) { + static constexpr auto kMode = + CreditBasedFlowControlMode::kLeCreditBasedFlowControl; + static constexpr ChannelParameters kChannelParams{ + .mode = kMode, + .max_rx_sdu_size = std::nullopt, + .flush_timeout = std::nullopt, + }; + static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ + .le_psm = 0x0015, + .src_cid = DynamicCid(), + .mtu = kDefaultMTU, + .mps = kMaxInboundPduPayloadSize, + .initial_credits = 0x0000, + }; + static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ + .dst_cid = DynamicCid(), + .mtu = 0x0064, + .mps = 0x0032, + .initial_credits = 0x0050, + .result = LECreditBasedConnectionResult::kSuccess, + }; + + auto chan = DoOpenOutbound(kLeConnReqPayload, + kLeConnRspPayload, + kChannelParams, + SignalingChannel::Status::kReject); + EXPECT_FALSE(chan); +} + +TEST_F(LeDynamicChannelTest, OpenOutboundPsmNotSupported) { + static constexpr auto kMode = + CreditBasedFlowControlMode::kLeCreditBasedFlowControl; + static constexpr ChannelParameters kChannelParams{ + .mode = kMode, + .max_rx_sdu_size = std::nullopt, + .flush_timeout = std::nullopt, + }; + static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ + .le_psm = 0x0015, + .src_cid = DynamicCid(), + .mtu = kDefaultMTU, + .mps = kMaxInboundPduPayloadSize, + .initial_credits = 0x0000, + }; + static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ + .dst_cid = DynamicCid(), + .mtu = 0x0064, + .mps = 0x0032, + .initial_credits = 0x0050, + .result = LECreditBasedConnectionResult::kPsmNotSupported, + }; + + auto chan = + DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); + EXPECT_FALSE(chan); +} + +} // namespace +} // namespace bt::l2cap::internal diff --git a/pw_bluetooth_sapphire/host/l2cap/logical_link.cc b/pw_bluetooth_sapphire/host/l2cap/logical_link.cc index 0e889e0fbd..09cc025822 100644 --- a/pw_bluetooth_sapphire/host/l2cap/logical_link.cc +++ b/pw_bluetooth_sapphire/host/l2cap/logical_link.cc @@ -18,6 +18,7 @@ #include #include +#include #include "pw_bluetooth_sapphire/internal/host/common/assert.h" #include "pw_bluetooth_sapphire/internal/host/common/log.h" @@ -26,7 +27,9 @@ #include "pw_bluetooth_sapphire/internal/host/l2cap/bredr_signaling_channel.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/channel.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/le_dynamic_channel.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/le_signaling_channel.h" +#include "pw_bluetooth_sapphire/internal/host/transport/link_type.h" #include "pw_bluetooth_sapphire/internal/host/transport/transport.h" namespace bt::l2cap::internal { @@ -108,7 +111,11 @@ LogicalLink::LogicalLink(hci_spec::ConnectionHandle handle, if (type_ == bt::LinkType::kLE) { signaling_channel_ = std::make_unique( OpenFixedChannel(kLESignalingChannelId), role_, pw_dispatcher_); - // TODO(armansito): Initialize LE registry when it exists. + dynamic_registry_ = std::make_unique( + signaling_channel_.get(), + fit::bind_member<&LogicalLink::OnChannelDisconnectRequest>(this), + fit::bind_member<&LogicalLink::OnServiceRequest>(this), + random_channel_ids); ServeConnectionParameterUpdateRequest(); } else { @@ -191,14 +198,7 @@ void LogicalLink::OpenChannel(Psm psm, ChannelParameters params, ChannelCallback callback) { BT_DEBUG_ASSERT(!closed_); - - // TODO(fxbug.dev/42178956): Implement channels for LE credit-based - // connections - if (type_ == bt::LinkType::kLE) { - bt_log(WARN, "l2cap", "not opening LE channel for PSM %.4x", psm); - CompleteDynamicOpen(/*dyn_chan=*/nullptr, std::move(callback)); - return; - } + BT_DEBUG_ASSERT(dynamic_registry_); auto create_channel = [this, cb = std::move(callback)](const DynamicChannel* dyn_chan) mutable { diff --git a/pw_bluetooth_sapphire/host/l2cap/logical_link_test.cc b/pw_bluetooth_sapphire/host/l2cap/logical_link_test.cc index 117b23ae2d..a40698137c 100644 --- a/pw_bluetooth_sapphire/host/l2cap/logical_link_test.cc +++ b/pw_bluetooth_sapphire/host/l2cap/logical_link_test.cc @@ -15,6 +15,7 @@ #include "pw_bluetooth_sapphire/internal/host/l2cap/logical_link.h" #include +#include #include "pw_bluetooth_sapphire/internal/host/hci-spec/protocol.h" #include "pw_bluetooth_sapphire/internal/host/hci/connection.h" @@ -24,6 +25,7 @@ #include "pw_bluetooth_sapphire/internal/host/testing/controller_test.h" #include "pw_bluetooth_sapphire/internal/host/testing/mock_controller.h" #include "pw_bluetooth_sapphire/internal/host/testing/test_packets.h" +#include "pw_bluetooth_sapphire/internal/host/transport/acl_data_channel.h" #include "pw_bluetooth_sapphire/internal/host/transport/link_type.h" namespace bt::l2cap::internal { @@ -58,7 +60,8 @@ class LogicalLinkTest : public TestingBase { TestingBase::TearDown(); } - void NewLogicalLink(bt::LinkType type = bt::LinkType::kLE) { + void NewLogicalLink(bt::LinkType type = bt::LinkType::kLE, + bool random_channel_ids = true) { const size_t kMaxPayload = kDefaultMTU; auto query_service_cb = [](hci_spec::ConnectionHandle, Psm) { return std::nullopt; @@ -73,14 +76,15 @@ class LogicalLinkTest : public TestingBase { std::move(query_service_cb), transport()->acl_data_channel(), transport()->command_channel(), - /*random_channel_ids=*/true, + random_channel_ids, *a2dp_offload_manager_, dispatcher()); } - void ResetAndCreateNewLogicalLink(LinkType type = LinkType::kACL) { + void ResetAndCreateNewLogicalLink(LinkType type = LinkType::kACL, + bool random_channel_ids = true) { link()->Close(); DeleteLink(); - NewLogicalLink(type); + NewLogicalLink(type, random_channel_ids); } LogicalLink* link() const { return link_.get(); } @@ -278,5 +282,43 @@ TEST_F(LogicalLinkTest, SetAutomaticFlushTimeoutSuccess) { *cb_status); } +TEST_F(LogicalLinkTest, OpensLeDynamicChannel) { + ResetAndCreateNewLogicalLink(LinkType::kLE, false); + static constexpr uint16_t kPsm = 0x015; + static constexpr ChannelParameters kParams{ + .mode = CreditBasedFlowControlMode::kLeCreditBasedFlowControl, + .max_rx_sdu_size = std::nullopt, + .flush_timeout = std::nullopt, + }; + + transport()->acl_data_channel()->SetDataRxHandler( + fit::bind_member<&LogicalLink::HandleRxPacket>(link())); + + const auto req = + l2cap::testing::AclLeCreditBasedConnectionReq(1, + kConnHandle, + kPsm, + kFirstDynamicChannelId, + kDefaultMTU, + kMaxInboundPduPayloadSize, + 0); + const auto rsp = l2cap::testing::AclLeCreditBasedConnectionRsp( + /*id=*/1, + /*link_handle=*/kConnHandle, + /*cid=*/kFirstDynamicChannelId, + /*mtu=*/64, + /*mps=*/32, + /*credits=*/10, + /*result=*/LECreditBasedConnectionResult::kSuccess); + EXPECT_ACL_PACKET_OUT(test_device(), req, &rsp); + + WeakPtr channel; + link()->OpenChannel( + kPsm, kParams, [&](auto result) { channel = std::move(result); }); + RunUntilIdle(); + + ASSERT_TRUE(channel.is_alive()); +} + } // namespace } // namespace bt::l2cap::internal diff --git a/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler.cc b/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler.cc index e4ae3f672d..f95d899a34 100644 --- a/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler.cc +++ b/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler.cc @@ -16,6 +16,8 @@ #include +#include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" + namespace bt::l2cap::internal { bool LowEnergyCommandHandler::ConnectionParameterUpdateResponse::Decode( const ByteBuffer& payload_buf) { @@ -26,6 +28,32 @@ bool LowEnergyCommandHandler::ConnectionParameterUpdateResponse::Decode( return true; } +bool LowEnergyCommandHandler::LeCreditBasedConnectionResponse::Decode( + const ByteBuffer& payload_buf) { + const uint16_t destination_cid = pw::bytes::ConvertOrderFrom( + cpp20::endian::little, + static_cast(payload_buf.ReadMember<&PayloadT::dst_cid>())); + destination_cid_ = ChannelId{destination_cid}; + const uint16_t mtu = pw::bytes::ConvertOrderFrom( + cpp20::endian::little, + static_cast(payload_buf.ReadMember<&PayloadT::mtu>())); + mtu_ = mtu; + const uint16_t mps = pw::bytes::ConvertOrderFrom( + cpp20::endian::little, + static_cast(payload_buf.ReadMember<&PayloadT::mps>())); + mps_ = mps; + const uint16_t initial_credits = pw::bytes::ConvertOrderFrom( + cpp20::endian::little, + static_cast( + payload_buf.ReadMember<&PayloadT::initial_credits>())); + initial_credits_ = initial_credits; + const uint16_t result = pw::bytes::ConvertOrderFrom( + cpp20::endian::little, + static_cast(payload_buf.ReadMember<&PayloadT::result>())); + result_ = LECreditBasedConnectionResult{result}; + return true; +} + LowEnergyCommandHandler::ConnectionParameterUpdateResponder:: ConnectionParameterUpdateResponder( SignalingChannel::Responder* sig_responder) @@ -43,6 +71,29 @@ LowEnergyCommandHandler::LowEnergyCommandHandler( SignalingChannelInterface* sig, fit::closure request_fail_callback) : CommandHandler(sig, std::move(request_fail_callback)) {} +bool LowEnergyCommandHandler::SendLeCreditBasedConnectionRequest( + uint16_t psm, + uint16_t cid, + uint16_t mtu, + uint16_t mps, + uint16_t credits, + SendLeCreditBasedConnectionRequestCallback cb) { + auto on_le_credit_based_connection_rsp = + BuildResponseHandler(std::move(cb)); + + LECreditBasedConnectionRequestPayload payload; + payload.le_psm = pw::bytes::ConvertOrderTo(cpp20::endian::little, psm); + payload.src_cid = pw::bytes::ConvertOrderTo(cpp20::endian::little, cid); + payload.mtu = pw::bytes::ConvertOrderTo(cpp20::endian::little, mtu); + payload.mps = pw::bytes::ConvertOrderTo(cpp20::endian::little, mps); + payload.initial_credits = + pw::bytes::ConvertOrderTo(cpp20::endian::little, credits); + + return sig()->SendRequest(kLECreditBasedConnectionRequest, + BufferView(&payload, sizeof(payload)), + std::move(on_le_credit_based_connection_rsp)); +} + bool LowEnergyCommandHandler::SendConnectionParameterUpdateRequest( uint16_t interval_min, uint16_t interval_max, diff --git a/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler_test.cc b/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler_test.cc index 87fc9d2941..a5c8f08819 100644 --- a/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler_test.cc +++ b/pw_bluetooth_sapphire/host/l2cap/low_energy_command_handler_test.cc @@ -17,6 +17,7 @@ #include #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_signaling_channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" namespace bt::l2cap::internal { @@ -192,4 +193,65 @@ TEST_F(LowEnergyCommandHandlerTest, InboundConnParamsUpdateReqNotEnoughBytes) { EXPECT_FALSE(cb_called); } +TEST_F(LowEnergyCommandHandlerTest, OutboundLeCreditBasedConnectionReq) { + constexpr static Psm kPsm = 0x1234; + constexpr static ChannelId kSourceCid = 0x0042; + constexpr static ChannelId kDestinationCid = 0x0054; + constexpr static uint16_t kLocalMtu = 126; + constexpr static uint16_t kRemoteMtu = 1234; + constexpr static uint16_t kLocalMps = 128; + constexpr static uint16_t kRemoteMps = 1000; + constexpr static uint16_t kLocalInitialCredits = 42; + constexpr static uint16_t kRemoteInitialCredits = 99; + constexpr static auto kResult = LECreditBasedConnectionResult::kNoResources; + const static StaticByteBuffer kLeConnReq(LowerBits(kPsm), + UpperBits(kPsm), + LowerBits(kSourceCid), + UpperBits(kSourceCid), + LowerBits(kLocalMtu), + UpperBits(kLocalMtu), + LowerBits(kLocalMps), + UpperBits(kLocalMps), + LowerBits(kLocalInitialCredits), + UpperBits(kLocalInitialCredits)); + const static StaticByteBuffer kLeConnRsp( + LowerBits(kDestinationCid), + UpperBits(kDestinationCid), + LowerBits(kRemoteMtu), + UpperBits(kRemoteMtu), + LowerBits(kRemoteMps), + UpperBits(kRemoteMps), + LowerBits(kRemoteInitialCredits), + UpperBits(kRemoteInitialCredits), + LowerBits(static_cast(kResult)), + UpperBits(static_cast(kResult))); + + bool cb_called = false; + LowEnergyCommandHandler::SendLeCreditBasedConnectionRequestCallback cb = + [&](const auto& rsp) { + cb_called = true; + EXPECT_EQ(SignalingChannel::Status::kSuccess, rsp.status()); + EXPECT_EQ(kDestinationCid, rsp.destination_cid()); + EXPECT_EQ(kRemoteMtu, rsp.mtu()); + EXPECT_EQ(kRemoteMps, rsp.mps()); + EXPECT_EQ(kRemoteInitialCredits, rsp.initial_credits()); + EXPECT_EQ(kResult, rsp.result()); + }; + + EXPECT_OUTBOUND_REQ(*fake_sig(), + kLECreditBasedConnectionRequest, + kLeConnReq.view(), + {SignalingChannel::Status::kSuccess, kLeConnRsp.view()}); + + EXPECT_TRUE( + cmd_handler()->SendLeCreditBasedConnectionRequest(kPsm, + kSourceCid, + kLocalMtu, + kLocalMps, + kLocalInitialCredits, + std::move(cb))); + RunUntilIdle(); + EXPECT_TRUE(cb_called); +} + } // namespace bt::l2cap::internal diff --git a/pw_bluetooth_sapphire/host/l2cap/test_packets.cc b/pw_bluetooth_sapphire/host/l2cap/test_packets.cc index e3b452c94a..666b886069 100644 --- a/pw_bluetooth_sapphire/host/l2cap/test_packets.cc +++ b/pw_bluetooth_sapphire/host/l2cap/test_packets.cc @@ -559,6 +559,88 @@ DynamicByteBuffer AclConnectionParameterUpdateRsp( UpperBits(static_cast(result)))); } +DynamicByteBuffer AclLeCreditBasedConnectionReq( + l2cap::CommandId id, + hci_spec::ConnectionHandle link_handle, + l2cap::Psm psm, + l2cap::ChannelId cid, + uint16_t mtu, + uint16_t mps, + uint16_t credits) { + return DynamicByteBuffer(StaticByteBuffer( + // ACL data header (link_handle, length: 18 bytes) + LowerBits(link_handle), + UpperBits(link_handle), + 0x12, + 0x00, + // L2CAP B-frame header: length 14, channel-id 5 (LE signaling) + 0x0e, + 0x00, + 0x05, + 0x00, + // LE credit based connection request, id 0x14, length 10 + l2cap::kLECreditBasedConnectionRequest, + id, + 0x0a, + 0x00, + // SPSM + LowerBits(psm), + UpperBits(psm), + // Source CID + LowerBits(cid), + UpperBits(cid), + // MTU + LowerBits(mtu), + UpperBits(mtu), + // MPS + LowerBits(mps), + UpperBits(mps), + // Initial Credits + LowerBits(credits), + UpperBits(credits))); +} + +DynamicByteBuffer AclLeCreditBasedConnectionRsp( + l2cap::CommandId id, + hci_spec::ConnectionHandle link_handle, + l2cap::ChannelId cid, + uint16_t mtu, + uint16_t mps, + uint16_t credits, + LECreditBasedConnectionResult result) { + return DynamicByteBuffer(StaticByteBuffer( + // ACL data header (link_handle, length: 18 bytes) + LowerBits(link_handle), + UpperBits(link_handle), + 0x12, + 0x00, + // L2CAP B-frame header: length 14, channel-id 5 (LE signaling) + 0x0e, + 0x00, + 0x05, + 0x00, + // LE credit based connection response, id 0x14, length 10 + l2cap::kLECreditBasedConnectionResponse, + id, + 0x0a, + 0x00, + // Destination CID + LowerBits(cid), + UpperBits(cid), + // MTU + LowerBits(mtu), + UpperBits(mtu), + // MPS + LowerBits(mps), + UpperBits(mps), + // Initial Credits + LowerBits(credits), + UpperBits(credits), + // Result + LowerBits(static_cast(result)), + UpperBits(static_cast(result)))); +} + DynamicByteBuffer AclSFrame(hci_spec::ConnectionHandle link_handle, l2cap::ChannelId channel_id, l2cap::internal::SupervisoryFunction function, diff --git a/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/le_dynamic_channel.h b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/le_dynamic_channel.h new file mode 100644 index 0000000000..1d81b7f4af --- /dev/null +++ b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/le_dynamic_channel.h @@ -0,0 +1,128 @@ +// Copyright 2024 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. + +#pragma once + +#include + +#include +#include + +#include "pw_bluetooth_sapphire/internal/host/l2cap/dynamic_channel_registry.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/signaling_channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/types.h" + +namespace bt::l2cap::internal { + +// Implements factories for LE dynamic channels and dispatches incoming +// signaling channel requests to the corresponding channels by local ID. +class LeDynamicChannelRegistry final : public DynamicChannelRegistry { + public: + LeDynamicChannelRegistry(SignalingChannelInterface* sig, + DynamicChannelCallback close_cb, + ServiceRequestCallback service_request_cb, + bool random_channel_ids); + ~LeDynamicChannelRegistry() override = default; + + private: + // DynamicChannelRegistry override + DynamicChannelPtr MakeOutbound(Psm psm, + ChannelId local_cid, + ChannelParameters params) override; + DynamicChannelPtr MakeInbound(Psm psm, + ChannelId local_cid, + ChannelId remote_cid, + ChannelParameters params) override; + + SignalingChannelInterface* const sig_; +}; + +struct LeChannelConfig { + // Maximum length of an SDU that can be received. + uint16_t mtu = 0; + // Maximum length of a PDU payload that can be received. + uint16_t mps = 0; + // Initial credits, this is only set at channel creation time. + uint16_t initial_credits = 0; +}; + +// Creates, configures, and tears down dynamic channels using the LE +// signaling channel. The lifetime of this object matches that of the channel +// itself: created in order to start an outbound channel or in response to an +// inbound channel request, then destroyed immediately after the channel is +// closed. This is intended to be created and owned by +// LeDynamicChannelRegistry. +class LeDynamicChannel final : public DynamicChannel { + public: + static std::unique_ptr MakeOutbound( + DynamicChannelRegistry* registry, + SignalingChannelInterface* signaling_channel, + Psm psm, + ChannelId local_cid, + ChannelParameters params); + + // DynamicChannel overrides + ~LeDynamicChannel() override = default; + + void Open(fit::closure open_cb) override; + void Disconnect(DisconnectDoneCallback done_cb) override; + bool IsConnected() const override; + bool IsOpen() const override; + + // Must not be called until channel is open. + ChannelInfo info() const override; + + /// The setup state of an LE dynamic channel is much simpler than a BR/EDR + /// channel, namely it does not have a configuration state machine. Instead, + /// it is considered configured as soon as the + /// L2CAP_(LE_)_CREDIT_BASED_CONNECTION_RSP is sent. + struct State { + // L2CAP_LE_CREDIT_BASED_CONNECTION_REQ or L2CAP_CREDIT_BASED_CONNECTION_REQ + // transmitted in either direction. + bool exchanged_connection_request = false; + // L2CAP_LE_CREDIT_BASED_CONNECTION_RSP or L2CAP_CREDIT_BASED_CONNECTION_RSP + // transmitted in opposite direction of REQ. + bool exchanged_connection_response = false; + // L2CAP_DISCONNECTION_REQ transmitted in either direction. + bool exchanged_disconnect_request = false; + + // Produce a string representation of `State`. + std::string ToString() const; + }; + + private: + LeDynamicChannel(DynamicChannelRegistry* registry, + SignalingChannelInterface* signaling_channel, + Psm psm, + ChannelId local_cid, + ChannelId remote_cid, + ChannelParameters params); + + void TriggerOpenCallback(); + void OnRxLeCreditConnRsp( + const LowEnergyCommandHandler::LeCreditBasedConnectionResponse& rsp); + + SignalingChannelInterface* const signaling_channel_; + CreditBasedFlowControlMode const flow_control_mode_; + + State state_; + LeChannelConfig local_config_; + std::optional remote_config_; + fit::closure open_result_cb_; + WeakSelf weak_self_; // Keep last. +}; + +} // namespace bt::l2cap::internal diff --git a/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h index 0049908027..4996b0da84 100644 --- a/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h +++ b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/low_energy_command_handler.h @@ -20,6 +20,30 @@ namespace bt::l2cap::internal { class LowEnergyCommandHandler final : public CommandHandler { public: + class LeCreditBasedConnectionResponse final : public Response { + public: + using PayloadT = LECreditBasedConnectionResponsePayload; + static constexpr const char* kName = "LE Credit Based Connection Response"; + + using Response::Response; // Inherit ctor + bool Decode(const ByteBuffer& payload_buf); + + ChannelId destination_cid() const { return destination_cid_; } + uint16_t mtu() const { return mtu_; } + uint16_t mps() const { return mps_; } + uint16_t initial_credits() const { return initial_credits_; } + LECreditBasedConnectionResult result() const { return result_; } + + private: + friend class LowEnergyCommandHandler; + + ChannelId destination_cid_; + uint16_t mtu_; + uint16_t mps_; + uint16_t initial_credits_; + LECreditBasedConnectionResult result_; + }; + class ConnectionParameterUpdateResponse final : public Response { public: using PayloadT = ConnectionParameterUpdateResponsePayload; @@ -58,6 +82,16 @@ class LowEnergyCommandHandler final : public CommandHandler { // non-empty. The callbacks are wrapped and moved into the SignalingChannel // and may outlive LowEnergyCommandHandler. + using SendLeCreditBasedConnectionRequestCallback = + fit::function; + bool SendLeCreditBasedConnectionRequest( + uint16_t psm, + uint16_t cid, + uint16_t mtu, + uint16_t mps, + uint16_t credits, + SendLeCreditBasedConnectionRequestCallback cb); + using ConnectionParameterUpdateResponseCallback = fit::function; bool SendConnectionParameterUpdateRequest( diff --git a/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/test_packets.h b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/test_packets.h index 56f749aa75..85e4d0e15a 100644 --- a/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/test_packets.h +++ b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/test_packets.h @@ -78,6 +78,24 @@ DynamicByteBuffer AclConnectionParameterUpdateRsp( hci_spec::ConnectionHandle link_handle, ConnectionParameterUpdateResult result); +DynamicByteBuffer AclLeCreditBasedConnectionReq( + l2cap::CommandId id, + hci_spec::ConnectionHandle link_handle, + l2cap::Psm psm, + l2cap::ChannelId cid, + uint16_t mtu, + uint16_t mps, + uint16_t credits); + +DynamicByteBuffer AclLeCreditBasedConnectionRsp( + l2cap::CommandId id, + hci_spec::ConnectionHandle link_handle, + l2cap::ChannelId cid, + uint16_t mtu, + uint16_t mps, + uint16_t credits, + LECreditBasedConnectionResult result); + // S-Frame Packets DynamicByteBuffer AclSFrame(hci_spec::ConnectionHandle link_handle, diff --git a/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/types.h b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/types.h index bc8d9eb3a9..389f67b3ea 100644 --- a/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/types.h +++ b/pw_bluetooth_sapphire/public/pw_bluetooth_sapphire/internal/host/l2cap/types.h @@ -166,6 +166,24 @@ struct ChannelInfo { flush_timeout); } + static ChannelInfo MakeCreditBasedFlowControlMode( + CreditBasedFlowControlMode mode, + uint16_t max_rx_sdu_size, + uint16_t max_tx_sdu_size, + uint16_t max_tx_pdu_payload_size, + uint16_t remote_initial_credits, + std::optional psm = std::nullopt) { + return ChannelInfo(mode, + max_rx_sdu_size, + max_tx_sdu_size, + /*n_frames_in_tx_window*/ 0, + /*max_transmissions*/ 0, + max_tx_pdu_payload_size, + psm, + std::nullopt, + remote_initial_credits); + } + ChannelInfo(AnyChannelMode mode, uint16_t max_rx_sdu_size, uint16_t max_tx_sdu_size, @@ -174,7 +192,8 @@ struct ChannelInfo { uint16_t max_tx_pdu_payload_size, std::optional psm = std::nullopt, std::optional flush_timeout = - std::nullopt) + std::nullopt, + std::optional remote_initial_credits = std::nullopt) : mode(mode), max_rx_sdu_size(max_rx_sdu_size), max_tx_sdu_size(max_tx_sdu_size), @@ -182,7 +201,8 @@ struct ChannelInfo { max_transmissions(max_transmissions), max_tx_pdu_payload_size(max_tx_pdu_payload_size), psm(psm), - flush_timeout(flush_timeout) {} + flush_timeout(flush_timeout), + remote_initial_credits(remote_initial_credits) {} AnyChannelMode mode; uint16_t max_rx_sdu_size; @@ -201,6 +221,9 @@ struct ChannelInfo { // If present, the channel's packets will be marked as flushable. The value // will be used to configure the link's automatic flush timeout. std::optional flush_timeout; + + // Only present for credit-based flow-control channels. + std::optional remote_initial_credits; }; // Data stored for services registered by higher layers.