blob: 8e1b1da71f1fd7bd50d9a0318c0f313561a6cd9b [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/l2cap/credit_based_flow_control_tx_engine.h"
#include <algorithm>
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/assert.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/log.h"
#include <pw_bluetooth/l2cap_frames.emb.h>
namespace bt::l2cap::internal {
namespace {
namespace emboss = pw::bluetooth::emboss;
constexpr auto kLeCreditBasedFlowControlMode =
CreditBasedFlowControlMode::kLeCreditBasedFlowControl;
constexpr size_t kMinimumLeMtu = 23;
constexpr size_t kMinimumLeMps = 23;
constexpr auto kPduHeaderSize = emboss::KFramePduHeader::IntrinsicSizeInBytes();
constexpr auto kSduHeaderSize = emboss::KFrameSduHeader::IntrinsicSizeInBytes();
constexpr uint32_t kMaxCredits = 65535;
template <typename Enum>
constexpr typename std::underlying_type<Enum>::type EnumValue(const Enum& e) {
return static_cast<typename std::underlying_type<Enum>::type>(e);
}
// Returns the payload size of the next PDU needed to transmit the maximal
// amount of the remaining |total_payload_size| bytes.
uint16_t NextPduPayloadSize(size_t total_payload_size, uint16_t max_pdu_size) {
BT_DEBUG_ASSERT(max_pdu_size > kPduHeaderSize);
// Factor in the header size.
uint16_t max_payload_size = max_pdu_size - kPduHeaderSize;
// There is no risk of overflow in this static cast as any value of
// `total_payload_size` larger than uint16_t max will be bounded by the
// smaller value in `max_payload_size`.
return static_cast<uint16_t>(
std::min<size_t>(total_payload_size, max_payload_size));
}
// Create and initialize a K-Frame with appropriate PDU header.
// Returns a tuple of the frame itself and a view of the payload.
std::tuple<DynamicByteBuffer, MutableBufferView> CreateFrame(
uint16_t payload_size, uint16_t channel_id) {
uint16_t pdu_size = kPduHeaderSize + payload_size;
DynamicByteBuffer buf(pdu_size);
auto header =
emboss::KFramePduHeaderWriter(buf.mutable_data(), kPduHeaderSize);
header.pdu_length().Write(payload_size);
header.channel_id().Write(channel_id);
MutableBufferView payload = buf.mutable_view(kPduHeaderSize);
return std::make_tuple(std::move(buf), payload);
}
} // namespace
CreditBasedFlowControlTxEngine::CreditBasedFlowControlTxEngine(
ChannelId channel_id,
uint16_t max_tx_sdu_size,
TxChannel& channel,
CreditBasedFlowControlMode mode,
uint16_t max_tx_pdu_size,
uint16_t initial_credits)
: TxEngine(channel_id, max_tx_sdu_size, channel),
mode_(mode),
max_tx_pdu_size_(max_tx_pdu_size),
credits_(initial_credits) {
// The enhanced flow control mode is not yet supported.
BT_ASSERT_MSG(mode_ == kLeCreditBasedFlowControlMode,
"Credit based flow control mode unsupported: 0x%.2ux",
EnumValue(mode));
BT_DEBUG_ASSERT_MSG(
mode != kLeCreditBasedFlowControlMode || max_tx_sdu_size > kMinimumLeMtu,
"Invalid MTU for LE mode: %d",
max_tx_sdu_size);
BT_DEBUG_ASSERT_MSG(
mode != kLeCreditBasedFlowControlMode || max_tx_pdu_size > kMinimumLeMps,
"Invalid MPS for LE mode: %d",
max_tx_pdu_size);
}
CreditBasedFlowControlTxEngine::~CreditBasedFlowControlTxEngine() = default;
void CreditBasedFlowControlTxEngine::NotifySduQueued() { ProcessSdus(); }
bool CreditBasedFlowControlTxEngine::AddCredits(uint16_t credits) {
if (static_cast<uint32_t>(credits_) + credits > kMaxCredits)
return false;
credits_ += credits;
// If there are queued SDUs, use the newly added credits to send them.
ProcessSdus();
return true;
}
uint16_t CreditBasedFlowControlTxEngine::credits() const { return credits_; }
size_t CreditBasedFlowControlTxEngine::segments_count() const {
return segments_.size();
}
void CreditBasedFlowControlTxEngine::SegmentSdu(ByteBufferPtr sdu) {
size_t payload_remaining = sdu->size() + kSduHeaderSize;
do {
// A credit based flow control packet has a fairly simple segmentation
// scheme where each PDU contains (at most) |max_tx_pdu_size_| - header_size
// bytes. The only bit of (relatively minor) complexity is the SDU size
// field, which is only included in the first K-Frame/PDU of the SDU. This
// loop iterates over each potential packet (and will only execute once if
// the entire SDU fits in a single PDU).
DynamicByteBuffer frame;
MutableBufferView payload;
std::tie(frame, payload) = CreateFrame(
NextPduPayloadSize(payload_remaining, max_tx_pdu_size_), channel_id());
BT_DEBUG_ASSERT(payload.size() <= payload_remaining);
if (payload_remaining > sdu->size()) {
// First frame of the SDU, write the SDU header.
emboss::KFrameSduHeaderWriter header(payload.mutable_data(),
kSduHeaderSize);
header.sdu_length().Write(sdu->size());
payload = payload.mutable_view(kSduHeaderSize);
payload_remaining -= kSduHeaderSize;
BT_DEBUG_ASSERT(payload_remaining == sdu->size());
}
sdu->Copy(&payload, sdu->size() - payload_remaining, payload.size());
payload_remaining -= payload.size();
segments_.push_back(std::make_unique<DynamicByteBuffer>(std::move(frame)));
} while (payload_remaining > 0);
}
void CreditBasedFlowControlTxEngine::TrySendSegments() {
while (credits_ > 0 && !segments_.empty()) {
channel().SendFrame(std::move(segments_.front()));
segments_.pop_front();
--credits_;
}
}
void CreditBasedFlowControlTxEngine::ProcessSdus() {
TrySendSegments();
while (credits_ > 0) {
std::optional<ByteBufferPtr> sdu = channel().GetNextQueuedSdu();
if (!sdu)
break;
BT_ASSERT(*sdu);
if ((*sdu)->size() > max_tx_sdu_size()) {
bt_log(INFO,
"l2cap",
"SDU size exceeds channel TxMTU (channel-id: 0x%.4x)",
channel_id());
return;
}
SegmentSdu(std::move(*sdu));
TrySendSegments();
}
}
} // namespace bt::l2cap::internal