blob: c2d9b8b5df07fe8f3f7bd282dd8db6046c19d8e4 [file]
/*
*
* Copyright (c) 2026 Project CHIP Authors
* All rights reserved.
*
* 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
*
* http://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 <app/clusters/commissioning-proxy-server/CommissioningProxyBleTransport.h>
#include <app/clusters/commissioning-proxy-server/CommissioningProxyCluster.h>
#include <clusters/CommissioningProxy/Commands.h>
#include <lib/support/SetupDiscriminator.h>
#include <lib/support/logging/CHIPLogging.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/ConnectivityManager.h>
#include <algorithm>
#include <cstring>
#include <utility>
namespace chip {
namespace app {
namespace Clusters {
namespace CommissioningProxy {
using chip::Ble::BLEEndPoint;
using chip::Ble::BleLayer;
using Protocols::InteractionModel::Status;
CommissioningProxyBleTransport::CommissioningProxyBleTransport(CommissioningProxyBleAdapter & adapter,
TimerDelegate & timerDelegate) :
mAdapter(adapter),
mTimerDelegate(timerDelegate), mBgScan(mBgScanHardware, timerDelegate)
{}
CommissioningProxyBleTransport::~CommissioningProxyBleTransport()
{
// Safe to run twice, and required if the owner never called it: the timers and the
// installed BleLayer delegate would otherwise outlive this object.
Shutdown();
}
// ==================================================================
// Small helpers
// ==================================================================
BleLayer * CommissioningProxyBleTransport::GetBleLayer() const
{
return DeviceLayer::ConnectivityMgr().GetBleLayer();
}
CommissioningProxyBleTransport::EndpointSlot * CommissioningProxyBleTransport::FindSlot(uint16_t sessionId)
{
for (auto & slot : mEndpoints)
{
if (slot.inUse && slot.sessionId == sessionId)
{
return &slot;
}
}
return nullptr;
}
CommissioningProxyBleTransport::EndpointSlot * CommissioningProxyBleTransport::FindSlot(const BLEEndPoint * endpoint)
{
if (endpoint == nullptr)
{
return nullptr;
}
for (auto & slot : mEndpoints)
{
if (slot.inUse && slot.endpoint == endpoint)
{
return &slot;
}
}
return nullptr;
}
CommissioningProxyBleTransport::EndpointSlot * CommissioningProxyBleTransport::ClaimSlot()
{
for (auto & slot : mEndpoints)
{
if (!slot.inUse)
{
return &slot;
}
}
return nullptr;
}
bool CommissioningProxyBleTransport::AnySessionOpen() const
{
for (const auto & slot : mEndpoints)
{
if (slot.inUse)
{
return true;
}
}
return false;
}
CommissioningProxyBleTransport::ScanResultT CommissioningProxyBleTransport::MakeScanResult(ByteSpan address, uint16_t discriminator,
uint16_t vendorId, uint16_t productId)
{
ScanResultT r{};
r.address.SetNonNull(address);
r.transport = BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kBle);
r.discriminator = discriminator;
r.vendorID = static_cast<VendorId>(vendorId);
r.productID = productId;
// A BLE advertisement carries neither of these.
r.extendedData.SetNull();
r.wiFiBand.ClearValue();
return r;
}
void CommissioningProxyBleTransport::FailPendingConnect(Status status)
{
// Every caller has already verified a connect is pending; the check is repeated here
// because bugprone-unchecked-optional-access does not reason across functions, and the
// dereference below would otherwise fail the clang-tidy gate.
VerifyOrReturn(mPendingConnect.has_value());
mTimerDelegate.CancelTimer(&mConnectTimer);
// Move out and reset before answering, so a re-entrant callback sees no pending
// connect.
ConnectCtx ctx = std::move(*mPendingConnect);
mPendingConnect.reset();
if (app::CommandHandler * cmd = ctx.handle.Get())
{
cmd->AddStatus(ctx.path, status);
}
ResumeBgScanIfNeeded();
}
// ==================================================================
// ProxyBleDelegate
// ==================================================================
void CommissioningProxyBleTransport::ProxyBleDelegate::Install()
{
auto * layer = mOwner.GetBleLayer();
if (layer == nullptr)
{
ChipLogError(AppServer, "ProxyBleDelegate::Install: no BleLayer");
return;
}
if (layer->mBleTransport == this)
{
return;
}
mOriginalTransport = layer->mBleTransport;
layer->mBleTransport = this;
ChipLogProgress(AppServer, "ProxyBleDelegate: installed (original=%p)", (void *) mOriginalTransport);
}
void CommissioningProxyBleTransport::ProxyBleDelegate::Uninstall()
{
auto * layer = mOwner.GetBleLayer();
if (layer == nullptr)
{
return;
}
if (layer->mBleTransport == this)
{
layer->mBleTransport = mOriginalTransport;
ChipLogProgress(AppServer, "ProxyBleDelegate: uninstalled");
}
mOriginalTransport = nullptr;
}
void CommissioningProxyBleTransport::ProxyBleDelegate::OnBleConnectionComplete(BLEEndPoint * endpoint)
{
// Central role: BleLayer just wrapped a new L2CAP connection in an endpoint. If we
// have a pending proxy connect with no endpoint yet, this one is ours. Initiate the
// BTP handshake on it.
if (mOwner.mPendingConnect.has_value() && mOwner.mPendingConnect->endpoint == nullptr)
{
mOwner.mPendingConnect->endpoint = endpoint;
mOwner.mSessionlessEndpoint = endpoint;
CHIP_ERROR err = endpoint->StartConnect();
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyBleDelegate: StartConnect failed: %" CHIP_ERROR_FORMAT, err.Format());
// The endpoint's own DoClose path will fire OnEndPointConnectComplete with
// the error; that path completes the IM response below.
}
return;
}
if (mOriginalTransport != nullptr)
{
mOriginalTransport->OnBleConnectionComplete(endpoint);
}
}
void CommissioningProxyBleTransport::ProxyBleDelegate::OnBleConnectionError(CHIP_ERROR err)
{
// Central role: pre-BTP-handshake error (scan failure, L2CAP rejected, etc.).
if (mOwner.mPendingConnect.has_value() && mOwner.mPendingConnect->endpoint == nullptr)
{
ChipLogError(AppServer, "ProxyBleDelegate: OnBleConnectionError: %" CHIP_ERROR_FORMAT, err.Format());
mOwner.FailPendingConnect(Status::Failure);
return;
}
if (mOriginalTransport != nullptr)
{
mOriginalTransport->OnBleConnectionError(err);
}
}
void CommissioningProxyBleTransport::ProxyBleDelegate::OnEndPointConnectComplete(BLEEndPoint * endpoint, CHIP_ERROR err)
{
// BTP handshake completed (or failed). If this endpoint is ours, complete the
// ProxyConnectRequest exchange.
if (mOwner.mPendingConnect.has_value() && mOwner.mPendingConnect->endpoint == endpoint)
{
mOwner.mTimerDelegate.CancelTimer(&mOwner.mConnectTimer);
ConnectCtx ctx = std::move(*mOwner.mPendingConnect);
mOwner.mPendingConnect.reset();
app::CommandHandler * cmd = ctx.handle.Get();
if (err != CHIP_NO_ERROR || cmd == nullptr)
{
ChipLogError(AppServer, "ProxyBleDelegate: OnEndPointConnectComplete err=%" CHIP_ERROR_FORMAT " cmd=%p", err.Format(),
(void *) cmd);
if (cmd != nullptr)
{
cmd->AddStatus(ctx.path, Status::Failure);
}
if (endpoint != nullptr)
{
endpoint->Close();
}
// Clear the sentinel after Close(): if Close() fired synchronously (normal
// BTP failure), OnEndPointConnectionClosed already cleared it; if Close() was
// a no-op (StartConnect failed internally and the endpoint was already
// kState_Closed), the sentinel remains stale and must be cleared here.
mOwner.mSessionlessEndpoint = nullptr;
// BTP handshake failed: no session was established, so release the scanner
// back to a background scan paused for this connect.
mOwner.ResumeBgScanIfNeeded();
return;
}
if (ctx.cluster == nullptr)
{
ChipLogError(AppServer, "ProxyBleDelegate: cluster gone at connect complete; closing endpoint");
if (endpoint != nullptr)
{
endpoint->Close();
}
mOwner.mSessionlessEndpoint = nullptr;
cmd->AddStatus(ctx.path, Status::Failure);
mOwner.ResumeBgScanIfNeeded();
return;
}
EndpointSlot * slot = mOwner.ClaimSlot();
if (slot == nullptr)
{
// The cluster's MaxSessions gate should have rejected the ProxyConnectRequest
// before it ever reached the driver, so this means the two disagree.
ChipLogError(AppServer, "ProxyBleDelegate: no free session slot; closing endpoint");
if (endpoint != nullptr)
{
endpoint->Close();
}
mOwner.mSessionlessEndpoint = nullptr;
cmd->AddStatus(ctx.path, Status::ResourceExhausted);
mOwner.ResumeBgScanIfNeeded();
return;
}
mOwner.mSessionlessEndpoint = nullptr; // ownership moves to the slot below
uint16_t sessionId = ctx.cluster->Sessions().AllocSessionId();
slot->inUse = true;
slot->sessionId = sessionId;
slot->endpoint = endpoint;
ctx.cluster->Sessions().RegisterSession(sessionId, CapabilitiesBitmap::kBle, ctx.fabricIndex);
ChipLogProgress(AppServer, "ProxyConnectRequest: BLE connected, proxy session %u (disc %u)", sessionId, ctx.discriminator);
CHIP_ERROR stateErr = ctx.cluster->SetCPState(CommissioningProxyCluster::kState_CPConnected);
if (stateErr != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyBleDelegate: SetCPState failed: %" CHIP_ERROR_FORMAT, stateErr.Format());
}
Commands::ProxyConnectResponse::Type response;
response.sessionID = sessionId;
cmd->AddResponse(ctx.path, response);
return;
}
if (mOriginalTransport != nullptr)
{
mOriginalTransport->OnEndPointConnectComplete(endpoint, err);
}
}
void CommissioningProxyBleTransport::ProxyBleDelegate::OnEndPointMessageReceived(BLEEndPoint * endpoint,
System::PacketBufferHandle && msg)
{
if (EndpointSlot * slot = mOwner.FindSlot(endpoint))
{
// Data that cannot be forwarded terminates the session. Abort() closes the
// connection without waiting for a transmission to drain, and fires
// OnEndPointConnectionClosed() synchronously, which tears the session down, so
// the slot must not be used after it.
CHIP_ERROR err = CommissioningProxySessionManager::ValidateCommissioneeMessage(ByteSpan(msg->Start(), msg->DataLength()));
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyBleDelegate: invalid message from commissionee on proxy session %u: %" CHIP_ERROR_FORMAT,
slot->sessionId, err.Format());
endpoint->Abort();
return;
}
// DispatchMessageResponse copies the payload into the IM ProxyMessageResponse
// synchronously, so msg can be released when this scope returns.
if (mOwner.mHost != nullptr)
{
mOwner.mHost->Sessions().DispatchMessageResponse(slot->sessionId, msg->Start(), msg->DataLength());
}
return;
}
if (mOriginalTransport != nullptr)
{
mOriginalTransport->OnEndPointMessageReceived(endpoint, std::move(msg));
}
}
void CommissioningProxyBleTransport::ProxyBleDelegate::OnEndPointConnectionClosed(BLEEndPoint * endpoint, CHIP_ERROR err)
{
if (EndpointSlot * slot = mOwner.FindSlot(endpoint))
{
const uint16_t sid = slot->sessionId;
ChipLogProgress(AppServer, "ProxyBleDelegate: session %u closed: %" CHIP_ERROR_FORMAT, sid, err.Format());
// Free the slot before notifying, so a re-entrant callback cannot find it.
*slot = EndpointSlot{};
// Fail any in-flight ProxyMessageRequest for this session and drop it.
if (mOwner.mHost != nullptr)
{
mOwner.mHost->Sessions().DispatchMessageFailure(sid, Status::Failure);
mOwner.mHost->Sessions().RemoveSession(sid);
// The cluster owns this decision: it can see the sessions of every transport.
mOwner.mHost->SetDisconnectedIfLastSession();
}
// Peer (commissionee) closed the session. Unlike ProxyDisconnectRequest, this
// path does not run through the dispatcher's OnAllSessionsClosed(), so resume a
// background scan paused for the connect once the last session is gone and no
// connect is mid-flight.
if (!mOwner.AnySessionOpen() && !mOwner.mPendingConnect.has_value())
{
mOwner.ResumeBgScanIfNeeded();
}
return;
}
// A close this transport initiated: a mid-BTP-handshake endpoint torn down by
// timeout/cancel/failure, or a promoted session closed by Disconnect(). Neither was
// passed to mOriginalTransport, so do not forward the close notification to it.
if (endpoint != nullptr && endpoint == mOwner.mSessionlessEndpoint)
{
mOwner.mSessionlessEndpoint = nullptr;
return;
}
if (mOriginalTransport != nullptr)
{
mOriginalTransport->OnEndPointConnectionClosed(endpoint, err);
}
}
CHIP_ERROR CommissioningProxyBleTransport::ProxyBleDelegate::SetEndPoint(BLEEndPoint * endpoint)
{
if (mOriginalTransport != nullptr)
{
return mOriginalTransport->SetEndPoint(endpoint);
}
return CHIP_NO_ERROR;
}
// ==================================================================
// Background-scan hardware hooks
// ==================================================================
CHIP_ERROR CommissioningProxyBleTransport::BgScanHardware::StartHardwareScan()
{
return mOwner.mAdapter.StartScan(HandleBackgroundScanResult, &mOwner);
}
void CommissioningProxyBleTransport::BgScanHardware::StopHardwareScan()
{
mOwner.mAdapter.StopScan();
}
void CommissioningProxyBleTransport::BgScanHardware::ClearCachedResults(BitMask<WiFiBandBitmap> bands)
{
// BLE results carry no band, so this always arrives as a whole-transport clear.
if (mOwner.mHost != nullptr)
{
mOwner.mHost->ScanCache().ClearTransport(CapabilitiesBitmap::kBle, bands);
}
}
// ==================================================================
// Adapter discovery callbacks
// ==================================================================
void CommissioningProxyBleTransport::HandleBackgroundScanResult(void * context, ByteSpan address, uint16_t discriminator,
uint16_t vendorId, uint16_t productId)
{
auto * self = static_cast<CommissioningProxyBleTransport *>(context);
// Result cache, TTL and the combined MaxCachedResults cap all live in the cluster's
// scan cache; this just builds the kBle result and reports it.
if (self != nullptr && self->mHost != nullptr)
{
self->mHost->ScanCache().Report(MakeScanResult(address, discriminator, vendorId, productId));
}
}
void CommissioningProxyBleTransport::HandleForegroundScanResult(void * context, ByteSpan address, uint16_t discriminator,
uint16_t vendorId, uint16_t productId)
{
auto * self = static_cast<CommissioningProxyBleTransport *>(context);
VerifyOrReturn(self != nullptr);
// ProxyScanResponse spec rule (CommissioningProxy.adoc, ProxyScanResult field):
// "Each found device SHALL be reported once based on discriminator/VendorID/
// ProductID per transport." Transport is fixed to kBle here, so dedupe on the
// remaining three.
for (size_t i = 0; i < self->mScanResultCount; i++)
{
const ScanRecord & existing = self->mScanResults[i];
if (existing.discriminator == discriminator && existing.vendorId == vendorId && existing.productId == productId)
{
return;
}
}
// Bounded at insert rather than truncated at emit: the store is exactly
// MaxCachedResults deep, which is also the cap the ProxyScanResponse must respect.
if (self->mScanResultCount >= kMaxScanResults)
{
ChipLogProgress(AppServer, "Ble::Scan: result store full (%u); dropping discriminator=%u",
static_cast<unsigned>(kMaxScanResults), discriminator);
return;
}
ScanRecord & r = self->mScanResults[self->mScanResultCount];
r = ScanRecord{};
if (address.size() >= sizeof(r.mac))
{
memcpy(r.mac, address.data(), sizeof(r.mac));
}
r.discriminator = discriminator;
r.vendorId = vendorId;
r.productId = productId;
self->mScanResultCount++;
ChipLogProgress(AppServer, "Ble::Scan: discovered discriminator=%u vid=0x%04x pid=0x%04x", discriminator, vendorId, productId);
}
// ==================================================================
// BleLayer connect callbacks
// ==================================================================
void CommissioningProxyBleTransport::HandleConnectFound(void * appState, BLE_CONNECTION_OBJECT connObj)
{
auto * self = static_cast<CommissioningProxyBleTransport *>(appState);
VerifyOrReturn(self != nullptr);
if (!self->mPendingConnect.has_value())
{
ChipLogProgress(AppServer, "OnConnectFound: no pending connect ctx; ignoring stale callback");
return;
}
// Wrap the raw L2CAP connection into a BLEEndPoint (Central role, autoClose=true)
// via the no-callback NewBleConnectionByObject path so OnConnectionComplete in
// BleLayer creates the endpoint and dispatches to our BleLayerDelegate.
auto * layer = self->GetBleLayer();
if (layer == nullptr)
{
ChipLogError(AppServer, "OnConnectFound: BleLayer null");
self->FailPendingConnect(Status::Failure);
return;
}
CHIP_ERROR err = layer->NewBleConnectionByObject(connObj);
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "OnConnectFound: NewBleConnectionByObject failed: %" CHIP_ERROR_FORMAT, err.Format());
// No further callback will fire — clean up synchronously rather than waiting for
// the connect timer to expire.
self->FailPendingConnect(Status::Failure);
return;
}
// After this returns the delegate's OnBleConnectionComplete has run and either
// recorded the endpoint + started BTP, or failed (cleanup happens in
// OnBleConnectionError or the connect timeout).
}
void CommissioningProxyBleTransport::HandleConnectError(void * appState, CHIP_ERROR err)
{
auto * self = static_cast<CommissioningProxyBleTransport *>(appState);
VerifyOrReturn(self != nullptr);
if (!self->mPendingConnect.has_value())
{
ChipLogProgress(AppServer,
"OnConnectError: ignoring stale callback "
"(err: %" CHIP_ERROR_FORMAT ")",
err.Format());
return;
}
ChipLogError(AppServer, "ProxyConnectRequest: BLE scan/connect failed: %" CHIP_ERROR_FORMAT, err.Format());
self->FailPendingConnect(Status::Failure);
}
void CommissioningProxyBleTransport::OnConnectTimeout()
{
if (!mPendingConnect.has_value())
{
return; // Already resolved.
}
ChipLogError(AppServer, "ProxyConnectRequest: timeout waiting for BLE connect");
ConnectCtx ctx = std::move(*mPendingConnect);
mPendingConnect.reset();
// Stop the BleLayer's in-flight scan/connect, suppressing any later OnConnectFound /
// OnConnectError callbacks.
if (auto * layer = GetBleLayer())
{
CHIP_ERROR cancelErr = layer->CancelBleIncompleteConnection();
if (cancelErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "OnConnectTimeout: CancelBleIncompleteConnection: %" CHIP_ERROR_FORMAT, cancelErr.Format());
}
}
// If the endpoint was already created (BTP handshake in flight), close it.
// OnEndPointConnectionClosed checks mSessionlessEndpoint to keep the close away from
// mOriginalTransport, and clears it. Clear it here too: Close() does nothing on an
// endpoint that is already closed or still draining its tx queue, and a pointer left
// set would match a later endpoint recycled to the same pool address.
if (ctx.endpoint != nullptr)
{
ctx.endpoint->Close();
}
mSessionlessEndpoint = nullptr;
if (app::CommandHandler * cmd = ctx.handle.Get())
{
cmd->AddStatus(ctx.path, Status::Timeout);
}
ResumeBgScanIfNeeded();
}
void CommissioningProxyBleTransport::OnScanWindowExpired()
{
mAdapter.StopScan();
mScanInProgress = false;
// Hand the results to the cluster's scan aggregator; it owns the command handle and
// emits the combined ProxyScanResponse once every transport's sub-scan has reported.
if (mHost != nullptr)
{
// Clamp to the cluster's advertised cap as well as the store's own bound, so the
// aggregator's numberOfResults field (uint8_t) cannot overflow even if the two
// ever diverge.
const size_t count = std::min<size_t>(mScanResultCount, mHost->GetMaxCachedResults());
ScanResultT out[kMaxScanResults];
for (size_t i = 0; i < count; i++)
{
const ScanRecord & d = mScanResults[i];
out[i] = MakeScanResult(ByteSpan(d.mac, sizeof(d.mac)), d.discriminator, d.vendorId, d.productId);
}
mHost->ScanAggregator().Contribute(Span<const ScanResultT>(out, count));
}
mScanResultCount = 0;
// The foreground scan has released the BLE scanner; resume a background scan that
// was paused to make room for it.
ResumeBgScanIfNeeded();
}
// ==================================================================
// CommissioningProxyTransport
// ==================================================================
void CommissioningProxyBleTransport::SetHost(CommissioningProxyCluster * host)
{
mHost = host;
}
Status CommissioningProxyBleTransport::Connect(app::CommandHandler * commandObj, const DataModel::InvokeRequest & request,
uint16_t discriminator, System::Clock::Seconds16 timeout)
{
if (mPendingConnect.has_value())
{
ChipLogError(AppServer, "ProxyConnectRequest: a BLE connect is already in progress");
return Status::Busy;
}
auto * layer = GetBleLayer();
if (layer == nullptr)
{
ChipLogError(AppServer, "ProxyConnectRequest: BleLayer null");
return Status::Failure;
}
// Sequential mode switch: the CP started in BLE peripheral mode so it could be
// commissioned onto the fabric (Step 1). The first BLE ProxyConnectRequest
// transitions BLE to central role so we can scan / connect to the commissionee
// (Step 2). Subsequent ProxyConnectRequest calls are no-ops.
{
CHIP_ERROR switchErr = mAdapter.EnableCentralRole();
if (switchErr == CHIP_ERROR_BUSY)
{
ChipLogError(AppServer, "ProxyConnectRequest: BLE busy with prior peripheral activity; retry shortly");
return Status::Busy;
}
if (switchErr != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyConnectRequest: EnableCentralRole failed: %" CHIP_ERROR_FORMAT, switchErr.Format());
return Status::Failure;
}
}
// Install our BleLayerDelegate wrapper; a second call does nothing.
mProxyDelegate.Install();
// The connect's NewBleConnectionByDiscriminator uses the single BLE scanner. Pause
// any running background scan so it does not contend for the scanner; it resumes
// once the session closes (OnAllSessionsClosed) or the connect fails below.
PauseBgScan();
// Per spec a Timeout of 0 indicates no timeout: the connect runs until it succeeds,
// fails, or is cancelled via ProxyDisconnectRequest(null).
const bool hasTimeout = (timeout.count() > 0);
ConnectCtx & ctx = mPendingConnect.emplace();
ctx.handle = app::CommandHandler::Handle(commandObj);
ctx.path = request.path;
ctx.discriminator = discriminator;
ctx.cluster = mHost;
ctx.fabricIndex = request.subjectDescriptor.fabricIndex;
commandObj->FlushAcksRightAwayOnSlowCommand();
if (auto * exchange = commandObj->GetExchangeContext())
{
// Keep the exchange open until just past the connect timeout, or disable the
// response timer entirely (kZero) when there is no timeout. Clamp the +5 s margin
// so a near-max timeout cannot wrap the uint16 seconds field.
const uint16_t timeoutSecs = timeout.count();
const uint16_t responseSecs =
(timeoutSecs > static_cast<uint16_t>(0xFFFF - 5)) ? 0xFFFF : static_cast<uint16_t>(timeoutSecs + 5);
exchange->SetResponseTimeout(hasTimeout ? System::Clock::Seconds16(responseSecs) : System::Clock::kZero);
}
SetupDiscriminator setupDisc;
setupDisc.SetLongValue(discriminator);
CHIP_ERROR err = layer->NewBleConnectionByDiscriminator(setupDisc, this, HandleConnectFound, HandleConnectError);
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyConnectRequest: NewBleConnectionByDiscriminator failed: %" CHIP_ERROR_FORMAT, err.Format());
mPendingConnect.reset();
ResumeBgScanIfNeeded();
return Status::Failure;
}
if (hasTimeout)
{
CHIP_ERROR timerErr = mTimerDelegate.StartTimer(&mConnectTimer, timeout);
if (timerErr != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyConnectRequest: StartTimer failed: %" CHIP_ERROR_FORMAT, timerErr.Format());
// Cancel the BLE scan we just started so we don't leak it.
(void) layer->CancelBleIncompleteConnection();
mPendingConnect.reset();
ResumeBgScanIfNeeded();
return Status::Failure;
}
}
ChipLogProgress(AppServer, "ProxyConnectRequest: BLE scan/connect started for discriminator %u", discriminator);
return Status::Success;
}
Status CommissioningProxyBleTransport::CancelPendingConnect(FabricIndex fabricIndex)
{
if (!mPendingConnect.has_value())
{
return Status::InvalidInState;
}
if (fabricIndex != mPendingConnect->fabricIndex)
{
ChipLogProgress(AppServer, "CancelPendingConnect: pending BLE connect owned by fabric %u, rejected fabric %u",
mPendingConnect->fabricIndex, fabricIndex);
return Status::NotFound;
}
mTimerDelegate.CancelTimer(&mConnectTimer);
ConnectCtx ctx = std::move(*mPendingConnect);
mPendingConnect.reset();
if (auto * layer = GetBleLayer())
{
CHIP_ERROR cancelErr = layer->CancelBleIncompleteConnection();
if (cancelErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "CancelPendingConnect: CancelBleIncompleteConnection: %" CHIP_ERROR_FORMAT,
cancelErr.Format());
}
}
// Close() lets OnEndPointConnectionClosed suppress forwarding to mOriginalTransport,
// and that callback clears the sentinel. Clear it here too: Close() does nothing on an
// endpoint that is already closed or still draining its tx queue, and a pointer left
// set would match a later endpoint recycled to the same pool address.
if (ctx.endpoint != nullptr)
{
ctx.endpoint->Close();
}
mSessionlessEndpoint = nullptr;
if (app::CommandHandler * cmd = ctx.handle.Get())
{
cmd->AddStatus(ctx.path, Status::Failure);
}
ResumeBgScanIfNeeded();
return Status::Success;
}
Status CommissioningProxyBleTransport::Disconnect(uint16_t sessionId)
{
EndpointSlot * slot = FindSlot(sessionId);
if (slot == nullptr)
{
return Status::NotFound;
}
BLEEndPoint * ep = slot->endpoint;
// Free the slot before Close so OnEndPointConnectionClosed (which fires
// synchronously) finds no matching session and does not call DispatchMessageFailure
// for this local-initiated disconnect. The endpoint is sessionless from here, so
// record it as such and the close is not forwarded to mOriginalTransport.
*slot = EndpointSlot{};
if (ep != nullptr)
{
mSessionlessEndpoint = ep;
ep->Close();
// Close() fires synchronously; the callback clears mSessionlessEndpoint. Clear
// explicitly here in case Close() was a no-op (already-closed endpoint).
mSessionlessEndpoint = nullptr;
}
return Status::Success;
}
CHIP_ERROR CommissioningProxyBleTransport::SendMessage(uint16_t sessionId, System::PacketBufferHandle && buf)
{
EndpointSlot * slot = FindSlot(sessionId);
if (slot == nullptr)
{
return CHIP_ERROR_KEY_NOT_FOUND;
}
if (slot->endpoint == nullptr)
{
return CHIP_ERROR_INCORRECT_STATE;
}
return slot->endpoint->Send(std::move(buf));
}
Status CommissioningProxyBleTransport::Scan(System::Clock::Seconds16 scanMaxTime)
{
if (mScanInProgress)
{
ChipLogProgress(AppServer, "Ble::Scan: scan already in progress — returning Busy");
return Status::Busy;
}
// The foreground scan and the background scan share the single BLE scanner. Pause any
// running background scan so StartScan below does not fail with BUSY; the scan window
// expiry resumes it when the foreground scan completes.
PauseBgScan();
mScanResultCount = 0;
CHIP_ERROR err = mAdapter.StartScan(HandleForegroundScanResult, this);
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "Ble::Scan: StartScan failed: %" CHIP_ERROR_FORMAT, err.Format());
ResumeBgScanIfNeeded();
return Status::Failure;
}
mScanInProgress = true;
CHIP_ERROR timerErr = mTimerDelegate.StartTimer(&mScanTimer, scanMaxTime);
if (timerErr != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "Ble::Scan: StartTimer failed: %" CHIP_ERROR_FORMAT, timerErr.Format());
mAdapter.StopScan();
mScanInProgress = false;
ResumeBgScanIfNeeded();
return Status::Failure;
}
ChipLogProgress(AppServer, "Ble::Scan: started, scanMaxTime=%us", scanMaxTime.count());
return Status::Success;
}
Status CommissioningProxyBleTransport::BgScanStart(System::Clock::Seconds16 timeout, BitMask<WiFiBandBitmap> /*wiFiBands*/,
FabricIndex fabricIndex, NodeId nodeId)
{
// BLE has no Wi-Fi bands: register with the kBle transport and empty bands. The
// registry starts or defers the hardware scan (StartScan reports BUSY while a connect
// or foreground scan holds the single scanner).
return mBgScan.Start(fabricIndex, nodeId, BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kBle), BitMask<WiFiBandBitmap>{},
timeout);
}
Status CommissioningProxyBleTransport::BgScanStop(BitMask<CapabilitiesBitmap> transport, BitMask<WiFiBandBitmap> /*wiFiBands*/,
FabricIndex fabricIndex, NodeId nodeId)
{
// BLE ignores Wi-Fi bands; pass the transport through with empty bands. The registry
// returns NOT_FOUND for an unknown fabric, SUCCESS when the request does not target
// this transport, and stops the scanner + clears the cache when the last fabric is
// removed.
return mBgScan.Stop(fabricIndex, nodeId, transport, BitMask<WiFiBandBitmap>{});
}
void CommissioningProxyBleTransport::OnFabricRemoved(FabricIndex fabricIndex)
{
// The cluster has already dropped the fabric's sessions; this drops its background
// scans so nothing survives into a reused FabricIndex.
mBgScan.RemoveFabric(fabricIndex);
}
void CommissioningProxyBleTransport::OnAllSessionsClosed()
{
// The proxy uses a one-way peripheral->central role switch on the first
// ProxyConnectRequest(kBle), so there is no peripheral advertising state to resume.
// But a background scan paused for the connect must be resumed now that the session
// has closed and the scanner is free again.
ResumeBgScanIfNeeded();
}
bool CommissioningProxyBleTransport::IsConnectPending() const
{
return mPendingConnect.has_value();
}
void CommissioningProxyBleTransport::Shutdown()
{
// Stop any foreground scan first. Its timer outlives the cluster otherwise, and the
// window expiry would then hand results to mHost — a cluster that no longer exists —
// and call ResumeBgScanIfNeeded(), restarting the radio after teardown.
mTimerDelegate.CancelTimer(&mScanTimer);
if (mScanInProgress)
{
mAdapter.StopScan();
mScanInProgress = false;
}
mScanResultCount = 0;
// Take the endpoints out first so the slots are free when Close() fires
// OnEndPointConnectionClosed synchronously — FindSlot returns nullptr and the
// callbacks are no-ops rather than mutating a slot mid-iteration.
BLEEndPoint * closing[kMaxSessions] = {};
size_t closingCount = 0;
for (auto & slot : mEndpoints)
{
if (slot.inUse && slot.endpoint != nullptr)
{
closing[closingCount++] = slot.endpoint;
}
slot = EndpointSlot{};
}
for (size_t i = 0; i < closingCount; i++)
{
closing[i]->Close();
}
// If a ProxyConnectRequest was in flight, cancel its timeout and fail the exchange.
// Do not call FailPendingConnect() here — that would invoke ResumeBgScanIfNeeded(),
// which would restart the HW scan immediately before the cleanup below stops and
// clears it.
if (mPendingConnect.has_value())
{
mTimerDelegate.CancelTimer(&mConnectTimer);
ConnectCtx ctx = std::move(*mPendingConnect);
mPendingConnect.reset();
// Stop the BleLayer's in-flight scan/connect, so no later OnConnectFound /
// OnConnectError reaches the torn-down transport.
if (auto * layer = GetBleLayer())
{
CHIP_ERROR cancelErr = layer->CancelBleIncompleteConnection();
if (cancelErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "Shutdown: CancelBleIncompleteConnection: %" CHIP_ERROR_FORMAT, cancelErr.Format());
}
}
// A BTP handshake in flight has an endpoint that was not in a slot, so the loop
// above did not close it. Clear the sentinel either way, as in OnConnectTimeout:
// Close() does nothing on an endpoint that is already closed or still draining its
// tx queue, so the callback that would normally clear it may never run.
if (ctx.endpoint != nullptr)
{
ctx.endpoint->Close();
}
mSessionlessEndpoint = nullptr;
if (app::CommandHandler * cmd = ctx.handle.Get())
{
cmd->AddStatus(ctx.path, Status::Failure);
}
}
// Tear down the background scan: cancel every per-fabric lifetime timer and stop the
// hardware scan if the registry currently owns it.
mBgScan.Shutdown();
// Restore BleLayer::mBleTransport so a later BLE event reaches the application's own
// delegate rather than this object after the cluster is gone.
mProxyDelegate.Uninstall();
// Last: the cluster is going away, so drop the pointer to it. Every callback above
// null-checks mHost, and any that still reaches the platform (a scan result already
// queued through ScheduleWork) must find null rather than a destroyed cluster.
mHost = nullptr;
}
} // namespace CommissioningProxy
} // namespace Clusters
} // namespace app
} // namespace chip