| // 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 <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_channel_manager.h" |
| #include "pw_bluetooth_proxy/internal/multibuf.h" |
| #include "pw_bluetooth_proxy/l2cap_channel_common.h" |
| #include "pw_log/log.h" |
| #include "pw_status/status.h" |
| |
| namespace pw::bluetooth::proxy::internal { |
| |
| namespace { |
| |
| // TODO: b/353734827 - Allow client to determine this constant. |
| const float kRxCreditReplenishThreshold = 0.30; |
| |
| } // namespace |
| |
| L2capCocInternal::L2capCocInternal(L2capCocInternal&& other) |
| : L2capChannel(static_cast<L2capChannel&&>(other)), |
| 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_; |
| } |
| } |
| |
| Status L2capCocInternal::DoCheckWriteParameter( |
| const FlatConstMultiBuf& payload) { |
| if (payload.size() > tx_mtu_) { |
| PW_LOG_ERROR( |
| "Payload (%zu bytes) exceeds MTU (%d bytes). So will not process. " |
| "local_cid: %#x, remote_cid: %#x, state: %u", |
| payload.size(), |
| tx_mtu_, |
| local_cid(), |
| remote_cid(), |
| cpp23::to_underlying(state())); |
| return Status::InvalidArgument(); |
| } |
| return pw::OkStatus(); |
| } |
| |
| pw::Result<L2capCocInternal> L2capCocInternal::Create( |
| MultiBufAllocator& rx_multibuf_allocator, |
| L2capChannelManager& l2cap_channel_manager, |
| uint16_t connection_handle, |
| ConnectionOrientedChannelConfig rx_config, |
| ConnectionOrientedChannelConfig tx_config, |
| ChannelEventCallback&& event_fn, |
| Function<void(FlatConstMultiBuf&& 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(); |
| } |
| |
| L2capCocInternal channel(/*rx_multibuf_allocator=*/rx_multibuf_allocator, |
| /*l2cap_channel_manager=*/l2cap_channel_manager, |
| /*connection_handle=*/connection_handle, |
| /*rx_config=*/rx_config, |
| /*tx_config=*/tx_config, |
| /*event_fn=*/std::move(event_fn), |
| /*receive_fn=*/std::move(receive_fn)); |
| |
| channel.Init(); |
| return channel; |
| } |
| |
| pw::Status L2capCocInternal::ReplenishRxCredits( |
| uint16_t additional_rx_credits) { |
| PW_CHECK(rx_multibuf_allocator()); |
| // SendFlowControlCreditInd logs if status is not ok, so no need to log here. |
| return channel_manager().SendFlowControlCreditInd(connection_handle(), |
| local_cid(), |
| additional_rx_credits, |
| *rx_multibuf_allocator()); |
| } |
| |
| pw::Status L2capCocInternal::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: L2capCocInternal::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 L2capCocInternal::DoHandlePduFromController(pw::span<uint8_t> kframe) { |
| if (state() != State::kRunning) { |
| PW_LOG_ERROR( |
| "btproxy: L2capCocInternal::HandlePduFromController on non-running " |
| "channel. local_cid: %u, remote_cid: %u, state: %u", |
| local_cid(), |
| remote_cid(), |
| cpp23::to_underlying(state())); |
| StopAndSendEvent(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()); |
| StopAndSendEvent(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()); |
| StopAndSendEvent(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()); |
| StopAndSendEvent(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()); |
| StopAndSendEvent(L2capChannelEvent::kRxInvalid); |
| return true; |
| } |
| |
| rx_sdu_ = MultiBufAdapter::Create(*rx_multibuf_allocator(), |
| 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()); |
| StopAndSendEvent(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_. |
| size_t copied = |
| MultiBufAdapter::Copy(rx_sdu_.value(), rx_sdu_offset_, kframe_payload); |
| if (copied < kframe_payload.size()) { |
| // 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()); |
| StopAndSendEvent(L2capChannelEvent::kRxInvalid); |
| return true; |
| } |
| |
| 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(MultiBufAdapter::Unwrap(rx_sdu_.value()))); |
| } |
| rx_sdu_ = std::nullopt; |
| rx_sdu_offset_ = 0; |
| } |
| |
| return true; |
| } |
| |
| bool L2capCocInternal::HandlePduFromHost(pw::span<uint8_t>) { |
| // Always forward data from host to controller |
| return false; |
| } |
| |
| L2capCocInternal::L2capCocInternal( |
| MultiBufAllocator& rx_multibuf_allocator, |
| L2capChannelManager& l2cap_channel_manager, |
| uint16_t connection_handle, |
| ConnectionOrientedChannelConfig rx_config, |
| ConnectionOrientedChannelConfig tx_config, |
| ChannelEventCallback&& event_fn, |
| Function<void(FlatConstMultiBuf&& payload)>&& receive_fn) |
| : L2capChannel(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)), |
| 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_); |
| } |
| |
| L2capCocInternal::~L2capCocInternal() { |
| // Don't log dtor of moved-from channels. |
| if (state() != State::kUndefined) { |
| PW_LOG_INFO("btproxy: L2capCoc dtor"); |
| } |
| } |
| |
| std::optional<uint16_t> L2capCocInternal::MaxBasicL2capPayloadSize() const { |
| std::optional<uint16_t> max_basic_l2cap_payload_size = |
| L2capChannel::MaxL2capPayloadSize(); |
| if (!max_basic_l2cap_payload_size) { |
| return std::nullopt; |
| } |
| return std::min(*max_basic_l2cap_payload_size, tx_mps_); |
| } |
| |
| std::optional<H4PacketWithH4> L2capCocInternal::GenerateNextTxPacket() { |
| std::lock_guard lock(tx_mutex_); |
| constexpr uint8_t kSduLengthFieldSize = |
| emboss::FirstKFrame::MinSizeInBytes() - |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes(); |
| std::optional<uint16_t> max_basic_payload_size = MaxBasicL2capPayloadSize(); |
| if (state() != State::kRunning || PayloadQueueEmpty() || tx_credits_ == 0 || |
| !max_basic_payload_size || |
| *max_basic_payload_size <= kSduLengthFieldSize) { |
| return std::nullopt; |
| } |
| |
| const FlatConstMultiBuf& sdu = GetFrontPayload(); |
| // 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_basic_payload_size - kSduLengthFieldSize; |
| sdu_bytes_in_segment = std::min(sdu.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_basic_payload_size; |
| sdu_bytes_in_segment = |
| std::min(sdu.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.size()); |
| PW_CHECK(first_kframe_writer->Ok()); |
| |
| MultiBufAdapter::Copy(first_kframe_writer->payload(), |
| sdu, |
| 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()); |
| MultiBufAdapter::Copy(subsequent_kframe_writer->payload(), |
| sdu, |
| tx_sdu_offset_, |
| sdu_bytes_in_segment); |
| } |
| |
| tx_sdu_offset_ += sdu_bytes_in_segment; |
| |
| if (tx_sdu_offset_ == sdu.size()) { |
| // This segment was the final (or only) PDU of the SDU payload. So all |
| // content has been copied from the front payload so it can be released. |
| PopFrontPayload(); |
| tx_sdu_offset_ = 0; |
| is_continuing_segment_ = false; |
| } else { |
| is_continuing_segment_ = true; |
| } |
| |
| --tx_credits_; |
| return h4_packet; |
| } |
| |
| void L2capCocInternal::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()); |
| StopAndSendEvent(L2capChannelEvent::kRxInvalid); |
| return; |
| } |
| |
| credits_previously_zero = tx_credits_ == 0; |
| tx_credits_ += credits; |
| } |
| if (credits_previously_zero) { |
| ReportNewTxPacketsOrCredits(); |
| } |
| } |
| |
| } // namespace pw::bluetooth::proxy::internal |