blob: 5a4183ad9328918cec60922106c33f359b3f6b6c [file]
// Copyright 2026 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/rfcomm/internal/rfcomm_channel_internal.h"
#include <limits>
#include <mutex>
#include "pw_assert/check.h"
#include "pw_bluetooth/emboss_util.h"
#include "pw_bluetooth/rfcomm_frames.emb.h"
#include "pw_bluetooth_proxy/internal/l2cap_channel.h"
#include "pw_bluetooth_proxy/rfcomm/rfcomm_common.h"
#include "pw_log/log.h"
#include "pw_multibuf/multibuf.h"
#include "pw_span/cast.h"
#include "pw_sync/lock_annotations.h"
#include "pw_sync/mutex.h"
namespace pw::bluetooth::proxy::rfcomm::internal {
BorrowedRfcommChannel::BorrowedRfcommChannel(RfcommChannelInternal& channel)
: channel_(channel) {
channel_.Borrow();
}
BorrowedRfcommChannel::~BorrowedRfcommChannel() { channel_.Unborrow(); }
RfcommChannelInternal::~RfcommChannelInternal() {
std::unique_lock lock(mutex_);
while (num_borrows_ > 0) {
// Release lock while waiting to avoid deadlock.
lock.unlock();
unborrowed_notification_.acquire();
lock.lock();
}
}
void RfcommChannelInternal::Borrow() {
std::lock_guard lock(mutex_);
num_borrows_++;
}
void RfcommChannelInternal::Unborrow() {
std::lock_guard lock(mutex_);
PW_CHECK(num_borrows_ > 0);
num_borrows_--;
if (num_borrows_ == 0) {
unborrowed_notification_.release();
}
}
RfcommChannelInternal::RfcommChannelInternal(
multibuf::MultiBufAllocator& multibuf_allocator,
ChannelProxy& l2cap_channel_proxy,
ConnectionHandle connection_handle,
uint8_t channel_number,
RfcommDirection direction,
bool mux_initiator,
const RfcommChannelConfig& rx_config,
const RfcommChannelConfig& tx_config,
const pw::checksum::Crc8& crc_calculator,
RfcommReceiveCallback&& receive_fn,
RfcommEventCallback&& event_fn)
: multibuf_allocator_(multibuf_allocator),
l2cap_channel_proxy_(l2cap_channel_proxy),
connection_handle_(connection_handle),
channel_number_(channel_number),
direction_(direction),
mux_initiator_(mux_initiator),
tx_config_(tx_config),
crc_calculator_(crc_calculator),
receive_fn_(std::move(receive_fn)),
event_fn_(std::move(event_fn)),
tx_credits_(tx_config.initial_credits),
rx_credits_(rx_config.initial_credits),
rx_total_credits_(rx_config.initial_credits) {
PW_LOG_INFO("RFCOMM channel %u (direction %u): created",
channel_number_,
static_cast<uint8_t>(direction_));
}
StatusWithMultiBuf RfcommChannelInternal::Write(multibuf::MultiBuf&& payload) {
std::lock_guard lock(mutex_);
std::lock_guard tx_lock(tx_mutex_);
if (state_ == State::kClosed) {
return {Status::NotFound(), std::move(payload)};
}
// If the queue is full, return an unavailable status and pass the payload
// back to the caller.
if (tx_queue_.full()) {
return {Status::Unavailable(), std::move(payload)};
}
tx_queue_.push(std::move(payload));
// If there are credits available, try to send the packet.
TryToSendPacket();
return {OkStatus()};
}
void RfcommChannelInternal::AddCredits(uint8_t credits) {
bool needs_notification = false;
{
std::lock_guard lock(mutex_);
std::lock_guard tx_lock(tx_mutex_);
if (state_ == State::kClosed) {
return;
}
// Add credits, preventing overflow.
if (std::numeric_limits<uint8_t>::max() - tx_credits_ < credits) {
PW_LOG_WARN(
"RFCOMM channel %u: TX credits overflow detected, capping at max.",
channel_number_);
tx_credits_ = std::numeric_limits<uint8_t>::max();
} else {
tx_credits_ += credits;
}
PW_LOG_DEBUG("RFCOMM channel %u: received %u TX credits, total is now %u",
channel_number_,
credits,
tx_credits_);
// If the queue is not empty, try to send the packet. Otherwise, notify the
// client that the channel is ready to send.
if (!tx_queue_.empty()) {
TryToSendPacket();
} else {
if (event_fn_) {
needs_notification = true;
}
}
}
if (needs_notification) {
event_fn_(RfcommEvent::kChannelReadyToSend);
}
}
void RfcommChannelInternal::TryToSendPacket()
PW_EXCLUSIVE_LOCKS_REQUIRED(tx_mutex_) {
// Prioritize sending credits over data.
if (pending_credit_tx_.has_value()) {
auto credit_pdu_span = pending_credit_tx_->ContiguousSpan().value();
if (!credit_pdu_span.empty()) {
auto credits_sent =
static_cast<uint16_t>(credit_pdu_span[credit_pdu_span.size() - 2]);
StatusWithMultiBuf write_status =
l2cap_channel_proxy_.Write(std::move(pending_credit_tx_.value()));
if (!write_status.status.ok()) {
if (write_status.buf.has_value() && !write_status.buf->empty()) {
pending_credit_tx_ = std::move(write_status.buf);
} else {
PW_LOG_WARN("Pending tx credit packet lost after write failure");
pending_credit_tx_.reset();
}
return;
}
pending_credit_tx_.reset();
std::lock_guard lock(rx_mutex_);
rx_credits_ += credits_sent;
} else {
PW_LOG_WARN("Invalid pending_credit_tx_ payload (size 0)");
pending_credit_tx_.reset();
return;
}
}
const size_t max_frame_size = tx_config_.max_frame_size;
// Try to send packets until the queue is empty or all credits are used.
while (tx_credits_ > 0 && !tx_queue_.empty()) {
// Get the first payload in the queue and the offset into the payload that
// will be sent next.
multibuf::MultiBuf& payload = tx_queue_.front();
size_t offset = send_packet_offset_;
// Calculate the size of the chunk to send. The chunk size is the minimum of
// the remaining payload size and the maximum frame size.
size_t chunk_size = std::min(payload.size() - offset, max_frame_size);
const bool use_long_header =
chunk_size >
static_cast<size_t>(emboss::RfcommMaxInfoSize::ONE_BYTE_LENGTH);
const size_t frame_size =
chunk_size +
static_cast<size_t>(
use_long_header
? emboss::RfcommDataFrameOverhead::WITH_LONG_HEADER
: emboss::RfcommDataFrameOverhead::WITH_SHORT_HEADER);
// Allocate a buffer for the RFCOMM packet.
auto result = multibuf_allocator_.AllocateContiguous(frame_size);
if (!result.has_value()) {
PW_LOG_ERROR(
"RFCOMM channel %u: Failed to allocate buffer for RFCOMM packet",
channel_number_);
break;
}
multibuf::MultiBuf new_buffer = std::move(result.value());
span<uint8_t> buffer =
span_cast<uint8_t>(new_buffer.ContiguousSpan().value());
auto frame_writer =
emboss::MakeRfcommFrameView(buffer.data(), buffer.size());
frame_writer.extended_address().Write(true);
frame_writer.command_response().Write(mux_initiator_);
frame_writer.direction().Write(direction_ == RfcommDirection::kInitiator);
frame_writer.channel().Write(channel_number_);
frame_writer.control().Write(pw::bluetooth::emboss::RfcommFrameType::
UNNUMBERED_INFORMATION_WITH_HEADER_CHECK);
if (use_long_header) {
frame_writer.length_extended_flag().Write(
pw::bluetooth::emboss::RfcommLengthExtended::EXTENDED);
frame_writer.length_extended().Write(chunk_size);
} else {
frame_writer.length_extended_flag().Write(
pw::bluetooth::emboss::RfcommLengthExtended::NORMAL);
frame_writer.length().Write(chunk_size);
}
auto information = frame_writer.information();
span<uint8_t> backing_storage(information.BackingStorage().data(),
information.SizeInBytes());
backing_storage = backing_storage.subspan(0, chunk_size);
auto bytes_copied =
payload.CopyTo(as_writable_bytes(backing_storage), offset);
PW_CHECK_UINT_EQ(bytes_copied.size(), backing_storage.size());
frame_writer.fcs().Write(crc_calculator_.Calculate(as_bytes(span(
buffer.data(),
static_cast<size_t>(emboss::RfcommHeaderLength::WITHOUT_LENGTH)))));
// Write the packet to the L2CAP channel.
StatusWithMultiBuf write_status =
l2cap_channel_proxy_.Write(std::move(new_buffer));
if (!write_status.status.ok()) {
// L2CAP channel is busy, we will retry later. The packet remains in the
// queue with its current offset.
break;
}
tx_credits_--;
send_packet_offset_ += chunk_size;
// If have sent the entire payload, pop the queue and reset the offset
// for the next packet.
if (send_packet_offset_ >= payload.size()) {
tx_queue_.pop();
send_packet_offset_ = 0;
}
}
}
void RfcommChannelInternal::Close(RfcommEvent event) {
{
std::lock_guard lock(mutex_);
std::lock_guard tx_lock(tx_mutex_);
if (state_ == State::kClosed) {
return;
}
state_ = State::kClosed;
// Clear the queue and reset the offset for the next packet. This is to
// avoid sending any packets after the channel is closed.
tx_queue_.clear();
send_packet_offset_ = 0;
}
// Notify the client that the channel is closed.
if (event_fn_) {
event_fn_(event);
}
}
Status RfcommChannelInternal::SendAdditionalRxCredits(uint8_t credits) {
{
std::lock_guard lock(rx_mutex_);
if (std::numeric_limits<uint8_t>::max() - credits < rx_total_credits_) {
rx_total_credits_ = std::numeric_limits<uint8_t>::max();
PW_LOG_WARN(
"RFCOMM channel %u: RX total credits overflow detected, "
"capping at max.",
channel_number_);
} else {
rx_total_credits_ += credits;
}
}
PW_LOG_DEBUG(
"Max credits increased by: %d (to %d)", credits, rx_total_credits_);
return SendCredits(credits);
}
// Sends credits (UIH frame with P-bit set) to the controller.
Status RfcommChannelInternal::SendCredits(uint8_t credits) {
std::lock_guard lock(tx_mutex_);
if (pending_credit_tx_.has_value()) {
PW_LOG_WARN("Earlier rx credit send pending. Skipping");
return Status::FailedPrecondition();
}
// RFCOMM frame with 1-byte length field and 1-byte credit.
auto frame_size =
static_cast<size_t>(emboss::RfcommDataFrameOverhead::WITH_LONG_HEADER);
auto result = multibuf_allocator_.AllocateContiguous(frame_size);
if (!result.has_value()) {
PW_LOG_ERROR("RFCOMM channel %u: Failed to allocate buffer for credits",
channel_number_);
return Status::ResourceExhausted();
}
multibuf::MultiBuf new_buffer = std::move(result.value());
span<uint8_t> buffer =
span_cast<uint8_t>(new_buffer.ContiguousSpan().value());
auto frame_writer = emboss::MakeRfcommFrameView(buffer.data(), buffer.size());
frame_writer.extended_address().Write(true);
frame_writer.command_response().Write(mux_initiator_);
frame_writer.direction().Write(direction_ == RfcommDirection::kInitiator);
frame_writer.channel().Write(channel_number_);
frame_writer.control().Write(
pw::bluetooth::emboss::RfcommFrameType::
UNNUMBERED_INFORMATION_WITH_HEADER_CHECK_AND_POLL_FINAL);
frame_writer.length_extended_flag().Write(
pw::bluetooth::emboss::RfcommLengthExtended::NORMAL);
frame_writer.length().Write(0);
frame_writer.credits().Write(credits);
// For UIH frames, FCS is calculated over the address and control fields.
frame_writer.fcs().Write(crc_calculator_.Calculate(as_bytes(
span(buffer.data(),
static_cast<size_t>(emboss::RfcommHeaderLength::WITHOUT_LENGTH)))));
pending_credit_tx_ = std::move(new_buffer);
TryToSendPacket();
return OkStatus();
}
bool RfcommChannelInternal::HandlePduFromController(uint8_t credits,
ConstByteSpan pdu) {
// Step 1: Handle credits if present.
if (credits > 0) {
AddCredits(credits);
}
// Step 2: Send data to the client.
if (receive_fn_ && !pdu.empty()) {
// TODO: https://pwbug.dev/478981478 - Rather than creating another
// MultiBuf when a MultiBuf already exists for this packet in
// RfcommChannelManager, return the payload part of the buffer.
auto result = multibuf_allocator_.AllocateContiguous(pdu.size());
if (!result.has_value()) {
PW_LOG_ERROR("Failed to allocate buffer for RFCOMM channel number %u",
channel_number_);
return true;
}
multibuf::MultiBuf& mbuf = result.value();
auto bytes_copied = mbuf.CopyFrom(pdu);
PW_CHECK(bytes_copied.ok());
receive_fn_(std::move(mbuf));
std::lock_guard lock(rx_mutex_);
if (rx_credits_ > 0) {
rx_credits_--;
} else {
PW_LOG_WARN(
"RFCOMM channel %u: received packet with no RX credits remaining.",
channel_number_);
}
}
// Step 3: Update RX credits and send credits if needed.
// Send credits if we have less than half of the initial credits to avoid
// underflow.
uint8_t credits_to_send = 0;
bool needs_to_send_credits = false;
{
std::lock_guard lock(rx_mutex_);
if (rx_credits_ <= rx_total_credits_ / 2) {
credits_to_send = rx_total_credits_ - rx_credits_;
if (credits_to_send > 0) {
needs_to_send_credits = true;
}
}
}
if (needs_to_send_credits && !SendCredits(credits_to_send).ok()) {
PW_LOG_WARN(
"RFCOMM channel %u: Failed to send RX credits, will retry later.",
channel_number_);
}
return false;
}
} // namespace pw::bluetooth::proxy::rfcomm::internal