blob: 3c9f022cec1d158fc221b8c92da3b6eb6f3f18d4 [file] [log] [blame] [edit]
// 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_proxy/l2cap_coc.h"
#include <cmath>
#include <cstdint>
#include <mutex>
#include "pw_assert/check.h"
#include "pw_bluetooth/emboss_util.h"
#include "pw_bluetooth/hci_data.emb.h"
#include "pw_bluetooth/l2cap_frames.emb.h"
#include "pw_bluetooth_proxy/h4_packet.h"
#include "pw_bluetooth_proxy/internal/l2cap_channel.h"
#include "pw_bluetooth_proxy/internal/l2cap_signaling_channel.h"
#include "pw_bluetooth_proxy/l2cap_channel_common.h"
#include "pw_bluetooth_proxy/single_channel_proxy.h"
#include "pw_log/log.h"
#include "pw_multibuf/multibuf.h"
#include "pw_status/status.h"
namespace pw::bluetooth::proxy {
namespace {
// TODO: b/353734827 - Allow client to determine this constant.
const float kRxCreditReplenishThreshold = 0.30;
} // namespace
L2capCoc::L2capCoc(L2capCoc&& other)
: SingleChannelProxy(std::move(static_cast<SingleChannelProxy&>(other))),
signaling_channel_(other.signaling_channel_),
rx_mtu_(other.rx_mtu_),
rx_mps_(other.rx_mps_),
tx_mtu_(other.tx_mtu_),
tx_mps_(other.tx_mps_),
receive_fn_(std::move(other.receive_fn_)) {
{
std::lock_guard lock(tx_mutex_);
std::lock_guard other_lock(other.tx_mutex_);
tx_credits_ = other.tx_credits_;
}
{
std::lock_guard lock(rx_mutex_);
std::lock_guard other_lock(other.rx_mutex_);
remaining_sdu_bytes_to_ignore_ = other.remaining_sdu_bytes_to_ignore_;
rx_sdu_ = std::move(other.rx_sdu_);
rx_sdu_offset_ = other.rx_sdu_offset_;
rx_sdu_bytes_remaining_ = other.rx_sdu_bytes_remaining_;
rx_remaining_credits_ = other.rx_remaining_credits_;
rx_total_credits_ = other.rx_total_credits_;
}
// Verify L2capChannel::holder_ and Holder::underlying_channel_ were properly
// set.
// TODO: https://pwbug.dev/388082771 - Being used for testing during
// transition. Delete when done.
CheckHolder(this);
CheckUnderlyingChannel(this);
}
Status L2capCoc::DoCheckWriteParameter(pw::multibuf::MultiBuf& payload) {
if (payload.size() > tx_mtu_) {
PW_LOG_ERROR(
"Payload (%zu bytes) exceeds MTU (%d bytes). So will not process.",
payload.size(),
tx_mtu_);
return Status::InvalidArgument();
}
return pw::OkStatus();
}
pw::Result<L2capCoc> L2capCoc::Create(
pw::multibuf::MultiBufAllocator& rx_multibuf_allocator,
L2capChannelManager& l2cap_channel_manager,
L2capSignalingChannel* signaling_channel,
uint16_t connection_handle,
CocConfig rx_config,
CocConfig tx_config,
ChannelEventCallback&& event_fn,
Function<void(multibuf::MultiBuf&& payload)>&& receive_fn) {
if (!AreValidParameters(/*connection_handle=*/connection_handle,
/*local_cid=*/rx_config.cid,
/*remote_cid=*/tx_config.cid)) {
return pw::Status::InvalidArgument();
}
if (tx_config.mps < emboss::L2capLeCreditBasedConnectionReq::min_mps() ||
tx_config.mps > emboss::L2capLeCreditBasedConnectionReq::max_mps()) {
PW_LOG_ERROR(
"Tx MPS (%d octets) invalid. L2CAP implementations shall support a "
"minimum MPS of 23 octets and may support an MPS up to 65533 octets.",
tx_config.mps);
return pw::Status::InvalidArgument();
}
return L2capCoc(
/*rx_multibuf_allocator=*/rx_multibuf_allocator,
/*l2cap_channel_manager=*/l2cap_channel_manager,
/*signaling_channel=*/signaling_channel,
/*connection_handle=*/connection_handle,
/*rx_config=*/rx_config,
/*tx_config=*/tx_config,
/*event_fn=*/std::move(event_fn),
/*receive_fn=*/std::move(receive_fn));
}
pw::Status L2capCoc::ReplenishRxCredits(uint16_t additional_rx_credits) {
if (!signaling_channel_) {
return Status::FailedPrecondition();
}
PW_CHECK(rx_multibuf_allocator());
// SendFlowControlCreditInd logs if status is not ok, so no need to log here.
return signaling_channel_->SendFlowControlCreditInd(
local_cid(), additional_rx_credits, *rx_multibuf_allocator());
}
pw::Status L2capCoc::SendAdditionalRxCredits(uint16_t additional_rx_credits) {
if (state() != State::kRunning) {
return Status::FailedPrecondition();
}
std::lock_guard lock(rx_mutex_);
Status status = ReplenishRxCredits(additional_rx_credits);
if (status.ok()) {
// We treat additional bumps from the client as bumping the total allowed
// credits.
rx_total_credits_ += additional_rx_credits;
rx_remaining_credits_ += additional_rx_credits;
PW_LOG_INFO(
"btproxy: L2capCoc::SendAdditionalRxCredits - status: %s, "
"additional_rx_credits: %u, rx_total_credits_: %u, "
"rx_remaining_credits_: %u",
status.str(),
additional_rx_credits,
rx_total_credits_,
rx_remaining_credits_);
}
DrainChannelQueuesIfNewTx();
return status;
}
bool L2capCoc::DoHandlePduFromController(pw::span<uint8_t> kframe) {
if (state() != State::kRunning) {
PW_LOG_ERROR(
"btproxy: L2capCoc::HandlePduFromController on non-running "
"channel. local_cid: %u, remote_cid: %u, state: %u",
local_cid(),
remote_cid(),
cpp23::to_underlying(state()));
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxWhileStopped);
return true;
}
std::lock_guard lock(rx_mutex_);
rx_remaining_credits_--;
uint16_t rx_credits_used = rx_total_credits_ - rx_remaining_credits_;
if (rx_credits_used >=
std::ceil(rx_total_credits_ * kRxCreditReplenishThreshold)) {
Status status = ReplenishRxCredits(rx_credits_used);
if (status.IsUnavailable()) {
PW_LOG_INFO(
"Unable to send %hu rx credits to remote (it has %hu credits "
"remaining). Will try on next PDU receive.",
rx_credits_used,
rx_total_credits_);
} else if (status.IsFailedPrecondition()) {
PW_LOG_WARN(
"Unable to send rx credits to remote, perhaps the connection has "
"been closed?");
} else {
PW_CHECK(status.ok());
rx_remaining_credits_ += rx_credits_used;
}
}
ConstByteSpan kframe_payload;
if (rx_sdu_bytes_remaining_ > 0) {
// Received PDU that is part of current SDU being assembled.
Result<emboss::SubsequentKFrameView> subsequent_kframe_view =
MakeEmbossView<emboss::SubsequentKFrameView>(kframe);
// Lower layers should not (and cannot) invoke this callback on a packet
// with an incomplete basic L2CAP header.
PW_CHECK_OK(subsequent_kframe_view);
// Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the payload size of any K-frame
// exceeds the receiver's MPS, the receiver shall disconnect the channel."
uint16_t payload_size = subsequent_kframe_view->payload_size().Read();
if (payload_size > rx_mps_) {
PW_LOG_ERROR(
"(CID %#x) Rx K-frame payload exceeds MPU. So stopping channel & "
"reporting it needs to be closed.",
local_cid());
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxInvalid);
return true;
}
kframe_payload =
as_bytes(span(subsequent_kframe_view->payload().BackingStorage().data(),
subsequent_kframe_view->payload_size().Read()));
} else {
// Received first (or only) PDU of SDU.
Result<emboss::FirstKFrameView> first_kframe_view =
MakeEmbossView<emboss::FirstKFrameView>(kframe);
if (!first_kframe_view.ok()) {
PW_LOG_ERROR(
"(CID %#x) Buffer is too small for first K-frame. So stopping "
"channel and reporting it needs to be closed.",
local_cid());
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxInvalid);
return true;
}
rx_sdu_bytes_remaining_ = first_kframe_view->sdu_length().Read();
// Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the SDU length field value
// exceeds the receiver's MTU, the receiver shall disconnect the channel."
if (rx_sdu_bytes_remaining_ > rx_mtu_) {
PW_LOG_ERROR(
"(CID %#x) Rx K-frame SDU exceeds MTU. So stopping channel & "
"reporting it needs to be closed.",
local_cid());
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxInvalid);
return true;
}
// Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the payload size of any K-frame
// exceeds the receiver's MPS, the receiver shall disconnect the channel."
uint16_t payload_size = first_kframe_view->payload_size().Read();
if (payload_size > rx_mps_) {
PW_LOG_ERROR(
"(CID %#x) Rx K-frame payload exceeds MPU. So stopping channel & "
"reporting it needs to be closed.",
local_cid());
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxInvalid);
return true;
}
rx_sdu_ =
rx_multibuf_allocator()->AllocateContiguous(rx_sdu_bytes_remaining_);
if (!rx_sdu_) {
PW_LOG_ERROR(
"(CID %#x) Rx MultiBuf allocator out of memory. So stopping channel "
"and reporting it needs to be closed.",
local_cid());
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxOutOfMemory);
return true;
}
kframe_payload =
as_bytes(span(first_kframe_view->payload().BackingStorage().data(),
first_kframe_view->payload_size().Read()));
}
// Copy segment into rx_sdu_.
StatusWithSize status = rx_sdu_->CopyFrom(/*source=*/kframe_payload,
/*position=*/rx_sdu_offset_);
if (status.IsResourceExhausted()) {
// Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the sum of the payload sizes
// for the K-frames exceeds the specified SDU length, the receiver shall
// disconnect the channel."
PW_LOG_ERROR(
"(CID %#x) Sum of K-frame payload sizes exceeds the specified SDU "
"length. So stopping channel and reporting it needs to be closed.",
local_cid());
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxInvalid);
return true;
}
PW_CHECK_OK(status);
rx_sdu_bytes_remaining_ -= kframe_payload.size();
rx_sdu_offset_ += kframe_payload.size();
if (rx_sdu_bytes_remaining_ == 0) {
// We have a full SDU, so invoke client callback.
if (receive_fn_) {
receive_fn_(std::move(*rx_sdu_));
}
rx_sdu_ = std::nullopt;
rx_sdu_offset_ = 0;
}
return true;
}
bool L2capCoc::HandlePduFromHost(pw::span<uint8_t>) {
// Always forward data from host to controller
return false;
}
void L2capCoc::DoClose() {
std::lock_guard lock(rx_mutex_);
signaling_channel_ = nullptr;
}
L2capCoc::L2capCoc(pw::multibuf::MultiBufAllocator& rx_multibuf_allocator,
L2capChannelManager& l2cap_channel_manager,
L2capSignalingChannel* signaling_channel,
uint16_t connection_handle,
CocConfig rx_config,
CocConfig tx_config,
ChannelEventCallback&& event_fn,
Function<void(multibuf::MultiBuf&& payload)>&& receive_fn)
: SingleChannelProxy(l2cap_channel_manager,
&rx_multibuf_allocator,
/*connection_handle=*/connection_handle,
/*transport=*/AclTransportType::kLe,
/*local_cid=*/rx_config.cid,
/*remote_cid=*/tx_config.cid,
/*payload_from_controller_fn=*/nullptr,
/*payload_from_host_fn=*/nullptr,
/*event_fn=*/std::move(event_fn)),
signaling_channel_(signaling_channel),
rx_mtu_(rx_config.mtu),
rx_mps_(rx_config.mps),
tx_mtu_(tx_config.mtu),
tx_mps_(tx_config.mps),
receive_fn_(std::move(receive_fn)),
rx_remaining_credits_(rx_config.credits),
rx_total_credits_(rx_config.credits),
tx_credits_(tx_config.credits) {
PW_LOG_INFO(
"btproxy: L2capCoc ctor - rx_remaining_credits_: %u, "
"rx_total_credits_: %u, tx_credits_: %u",
rx_remaining_credits_,
rx_total_credits_,
tx_credits_);
// Verify L2capChannel::holder_ and Holder::underlying_channel_ were properly
// set.
// TODO: https://pwbug.dev/388082771 - Being used for testing during
// transition. Delete when done.
CheckHolder(this);
CheckUnderlyingChannel(this);
}
L2capCoc::~L2capCoc() {
// Don't log dtor of moved-from channels.
if (state() != State::kUndefined) {
PW_LOG_INFO("btproxy: L2capCoc dtor");
}
}
std::optional<uint16_t> L2capCoc::MaxL2capPayloadSize() const {
std::optional<uint16_t> max_l2cap_payload_size =
L2capChannel::MaxL2capPayloadSize();
if (!max_l2cap_payload_size) {
return std::nullopt;
}
return std::min(*max_l2cap_payload_size, tx_mps_);
}
std::optional<H4PacketWithH4> L2capCoc::GenerateNextTxPacket() {
std::lock_guard lock(tx_mutex_);
constexpr uint8_t kSduLengthFieldSize = 2;
std::optional<uint16_t> max_l2cap_payload_size = MaxL2capPayloadSize();
if (state() != State::kRunning || PayloadQueueEmpty() || tx_credits_ == 0 ||
!max_l2cap_payload_size ||
*max_l2cap_payload_size <= kSduLengthFieldSize) {
return std::nullopt;
}
ConstByteSpan sdu_span = GetFrontPayloadSpan();
// Number of client SDU bytes to be encoded in this segment.
uint16_t sdu_bytes_in_segment;
// Size of PDU payload for this L2CAP frame.
uint16_t pdu_data_size;
if (!is_continuing_segment_) {
// Generating the first (or only) PDU of an SDU.
size_t sdu_bytes_max_allowable =
*max_l2cap_payload_size - kSduLengthFieldSize;
sdu_bytes_in_segment = std::min(sdu_span.size(), sdu_bytes_max_allowable);
pdu_data_size = sdu_bytes_in_segment + kSduLengthFieldSize;
} else {
// Generating a continuing PDU in an SDU.
size_t sdu_bytes_max_allowable = *max_l2cap_payload_size;
sdu_bytes_in_segment =
std::min(sdu_span.size() - tx_sdu_offset_, sdu_bytes_max_allowable);
pdu_data_size = sdu_bytes_in_segment;
}
pw::Result<H4PacketWithH4> h4_result = PopulateTxL2capPacket(pdu_data_size);
if (!h4_result.ok()) {
// This can fail if all H4 buffers are occupied.
return std::nullopt;
}
H4PacketWithH4 h4_packet = std::move(*h4_result);
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(h4_packet.GetHciSpan());
PW_CHECK(acl.ok());
if (!is_continuing_segment_) {
Result<emboss::FirstKFrameWriter> first_kframe_writer =
MakeEmbossWriter<emboss::FirstKFrameWriter>(
acl->payload().BackingStorage().data(),
acl->payload().SizeInBytes());
PW_CHECK(first_kframe_writer.ok());
first_kframe_writer->sdu_length().Write(sdu_span.size());
PW_CHECK(first_kframe_writer->Ok());
PW_CHECK(TryToCopyToEmbossStruct(
/*emboss_dest=*/first_kframe_writer->payload(),
/*src=*/sdu_span.subspan(tx_sdu_offset_, sdu_bytes_in_segment)));
} else {
Result<emboss::SubsequentKFrameWriter> subsequent_kframe_writer =
MakeEmbossWriter<emboss::SubsequentKFrameWriter>(
acl->payload().BackingStorage().data(),
acl->payload().SizeInBytes());
PW_CHECK(subsequent_kframe_writer.ok());
PW_CHECK(TryToCopyToEmbossStruct(
/*emboss_dest=*/subsequent_kframe_writer->payload(),
/*src=*/sdu_span.subspan(tx_sdu_offset_, sdu_bytes_in_segment)));
}
tx_sdu_offset_ += sdu_bytes_in_segment;
if (tx_sdu_offset_ == sdu_span.size()) {
// This segment was the final (or only) PDU of the SDU.
PopFrontPayload();
tx_sdu_offset_ = 0;
is_continuing_segment_ = false;
} else {
is_continuing_segment_ = true;
}
--tx_credits_;
return h4_packet;
}
void L2capCoc::AddTxCredits(uint16_t credits) {
if (state() != State::kRunning) {
PW_LOG_ERROR(
"(CID %#x) Received credits on stopped CoC. So will ignore signal.",
local_cid());
return;
}
bool credits_previously_zero;
{
std::lock_guard lock(tx_mutex_);
// Core Spec v6.0 Vol 3, Part A, 10.1: "The device receiving the credit
// packet shall disconnect the L2CAP channel if the credit count exceeds
// 65535."
if (credits > emboss::L2capLeCreditBasedConnectionReq::max_credit_value() -
tx_credits_) {
PW_LOG_ERROR(
"btproxy: Received additional tx credits %u which put tx_credits_ %u "
"beyond max credit value of %ld. So stopping channel and reporting "
"it needs to be closed. local_cid: %u, remote_cid: %u",
credits,
tx_credits_,
long{emboss::L2capLeCreditBasedConnectionReq::max_credit_value()},
local_cid(),
remote_cid());
StopUnderlyingChannelWithEvent(L2capChannelEvent::kRxInvalid);
return;
}
credits_previously_zero = tx_credits_ == 0;
tx_credits_ += credits;
}
if (credits_previously_zero) {
ReportNewTxPacketsOrCredits();
}
}
} // namespace pw::bluetooth::proxy