blob: e1fe534610bf071a11fa9b2c28578df4460f3365 [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/CommissioningProxyPafTransport.h>
#include <app/clusters/commissioning-proxy-server/CommissioningProxyCluster.h>
#include <clusters/CommissioningProxy/Commands.h>
#include <lib/support/logging/CHIPLogging.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/ConnectivityManager.h>
#include <wifipaf/WiFiPAFLayer.h>
#include <algorithm>
#include <cstring>
#include <utility>
namespace chip {
namespace app {
namespace Clusters {
namespace CommissioningProxy {
using Protocols::InteractionModel::Status;
CommissioningProxyPafTransport::CommissioningProxyPafTransport(CommissioningProxyPafAdapter & adapter,
TimerDelegate & timerDelegate) :
mAdapter(adapter),
mTimerDelegate(timerDelegate), mBgScan(mBgScanHardware, timerDelegate)
{}
CommissioningProxyPafTransport::~CommissioningProxyPafTransport()
{
// Safe to run twice, and required if the owner never called it: the connect timer and
// the installed WiFiPAF delegate would otherwise outlive this object.
Shutdown();
}
// ==================================================================
// Small helpers
// ==================================================================
CommissioningProxyPafTransport::SessionSlot * CommissioningProxyPafTransport::FindSlot(uint16_t sessionId)
{
for (auto & slot : mSessions)
{
if (slot.inUse && slot.sessionId == sessionId)
{
return &slot;
}
}
return nullptr;
}
CommissioningProxyPafTransport::SessionSlot * CommissioningProxyPafTransport::FindSlotByPeer(uint32_t peerId)
{
for (auto & slot : mSessions)
{
if (slot.inUse && slot.pafSession.peer_id == peerId)
{
return &slot;
}
}
return nullptr;
}
CommissioningProxyPafTransport::SessionSlot * CommissioningProxyPafTransport::ClaimSlot()
{
for (auto & slot : mSessions)
{
if (!slot.inUse)
{
return &slot;
}
}
return nullptr;
}
bool CommissioningProxyPafTransport::AnySessionOpen() const
{
for (const auto & slot : mSessions)
{
if (slot.inUse)
{
return true;
}
}
return false;
}
CommissioningProxyPafTransport::ScanResultT CommissioningProxyPafTransport::MakeScanResult(ByteSpan address, uint16_t discriminator,
uint16_t vendorId, uint16_t productId,
ByteSpan extendedData, uint16_t wiFiBand)
{
ScanResultT r{};
r.address.SetNonNull(address);
r.transport = BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kWiFiPAF);
r.discriminator = discriminator;
r.vendorID = static_cast<VendorId>(vendorId);
r.productID = productId;
if (!extendedData.empty())
{
r.extendedData.SetNonNull(extendedData);
}
else
{
r.extendedData.SetNull();
}
if (wiFiBand != 0)
{
r.wiFiBand.SetValue(static_cast<WiFiBandBitmap>(wiFiBand));
}
else
{
r.wiFiBand.ClearValue();
}
return r;
}
// ==================================================================
// ProxyPafDelegate
// ==================================================================
void CommissioningProxyPafTransport::ProxyPafDelegate::Install()
{
auto & layer = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer();
if (layer.mWiFiPAFTransport == this)
{
return;
}
mOriginalTransport = layer.mWiFiPAFTransport;
layer.mWiFiPAFTransport = this;
ChipLogProgress(AppServer, "ProxyPafDelegate: installed (original=%p)", (void *) mOriginalTransport);
}
void CommissioningProxyPafTransport::ProxyPafDelegate::Uninstall()
{
auto & layer = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer();
if (layer.mWiFiPAFTransport == this)
{
layer.mWiFiPAFTransport = mOriginalTransport;
ChipLogProgress(AppServer, "ProxyPafDelegate: uninstalled");
}
mOriginalTransport = nullptr;
}
CHIP_ERROR CommissioningProxyPafTransport::ProxyPafDelegate::WiFiPAFMessageReceived(WiFiPAF::WiFiPAFSession & rxInfo,
System::PacketBufferHandle && msg)
{
if (SessionSlot * slot = mOwner.FindSlotByPeer(rxInfo.peer_id))
{
// Data that cannot be forwarded terminates the session. Returning the error makes
// the PAFTP endpoint close itself, which reaches WiFiPAFCloseSession() below and
// tears the session down.
CHIP_ERROR err = CommissioningProxySessionManager::ValidateCommissioneeMessage(ByteSpan(msg->Start(), msg->DataLength()));
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyPafDelegate: invalid message from commissionee on proxy session %u: %" CHIP_ERROR_FORMAT,
slot->sessionId, err.Format());
return err;
}
// 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 CHIP_NO_ERROR;
}
if (mOriginalTransport != nullptr)
{
return mOriginalTransport->WiFiPAFMessageReceived(rxInfo, std::move(msg));
}
ChipLogError(AppServer, "ProxyPafDelegate: no original transport for non-proxy session");
return CHIP_ERROR_INCORRECT_STATE;
}
CHIP_ERROR CommissioningProxyPafTransport::ProxyPafDelegate::WiFiPAFMessageSend(WiFiPAF::WiFiPAFSession & txInfo,
System::PacketBufferHandle && msg)
{
if (mOriginalTransport != nullptr)
{
return mOriginalTransport->WiFiPAFMessageSend(txInfo, std::move(msg));
}
return CHIP_ERROR_INCORRECT_STATE;
}
CHIP_ERROR CommissioningProxyPafTransport::ProxyPafDelegate::WiFiPAFCloseSession(WiFiPAF::WiFiPAFSession & sessionInfo)
{
// The PAF session closed on its own — e.g. the ED dropped NAN when ConnectNetwork
// switched it to infrastructure Wi-Fi, so the PAFTP ack-received timer fired and the
// layer is closing the session here. There is no ProxyDisconnectRequest, so this
// handler runs the same teardown the cluster would on disconnect: fail any in-flight
// ProxyMessage, drop the session, terminate the NAN subscribe, and resume a
// background scan that was paused for the connect. (Mirrors the BLE transport's
// OnEndPointConnectionClosed handler.)
if (SessionSlot * slot = mOwner.FindSlotByPeer(sessionInfo.peer_id))
{
const uint16_t sid = slot->sessionId;
WiFiPAF::WiFiPAFSession pafSession = slot->pafSession;
ChipLogError(AppServer, "WiFiPAFCloseSession: PAF session for proxy session %u closed by peer — cleaning up", sid);
// Free the slot before notifying, so a re-entrant callback cannot find it.
*slot = SessionSlot{};
// Resolve any in-flight ProxyMessageRequest so the commissioner gets a timely
// error instead of waiting out its 30 s timeout.
if (mOwner.mHost != nullptr)
{
mOwner.mHost->Sessions().DispatchMessageFailure(sid, Status::Failure);
}
WiFiPAF::WiFiPAFLayer & layer = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer();
LogErrorOnFailure(layer.RmPafSession(WiFiPAF::PafInfoAccess::kAccSessionId, pafSession));
// Terminate the NAN subscribe (for a subscriber session the WiFiPAFSession id IS
// the subscribe_id) so it does not linger in wpa_supplicant — the same leak the
// ProxyDisconnect path guards against. Do NOT CloseEndPoint here: the layer is
// already closing the endpoint, which is why we were called.
if (pafSession.id != 0)
{
(void) DeviceLayer::ConnectivityMgr().WiFiPAFCancelSubscribe(pafSession.id);
}
if (mOwner.mHost != nullptr)
{
mOwner.mHost->Sessions().RemoveSession(sid);
// The cluster owns this decision: it can see the sessions of every transport.
mOwner.mHost->SetDisconnectedIfLastSession();
}
// This path does not run through the cluster'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.OnAllSessionsClosed();
}
}
if (mOriginalTransport != nullptr)
{
return mOriginalTransport->WiFiPAFCloseSession(sessionInfo);
}
return CHIP_NO_ERROR;
}
bool CommissioningProxyPafTransport::ProxyPafDelegate::WiFiPAFResourceAvailable()
{
if (mOriginalTransport != nullptr)
{
return mOriginalTransport->WiFiPAFResourceAvailable();
}
return true;
}
// ==================================================================
// Background-scan hardware hooks
// ==================================================================
CHIP_ERROR CommissioningProxyPafTransport::BgScanHardware::StartHardwareScan()
{
if (mOwner.mPendingConnect.has_value())
{
// A ProxyConnect owns the single NAN subscribe slot; defer.
return CHIP_ERROR_BUSY;
}
return mOwner.mAdapter.StartBackgroundScan(HandleBackgroundScanResult, &mOwner);
}
void CommissioningProxyPafTransport::BgScanHardware::StopHardwareScan()
{
mOwner.mAdapter.StopBackgroundScan();
}
void CommissioningProxyPafTransport::BgScanHardware::ClearCachedResults(BitMask<WiFiBandBitmap> bands)
{
// bands == 0 means the transport itself stopped; otherwise only those bands did, and
// only their results go. PAFTP results are the only ones carrying a band.
if (mOwner.mHost != nullptr)
{
mOwner.mHost->ScanCache().ClearTransport(CapabilitiesBitmap::kWiFiPAF, bands);
}
}
// ==================================================================
// Adapter discovery callbacks
// ==================================================================
void CommissioningProxyPafTransport::HandleBackgroundScanResult(void * context, ByteSpan address, uint16_t discriminator,
uint16_t vendorId, uint16_t productId, ByteSpan extendedData,
uint16_t wiFiBand)
{
auto * self = static_cast<CommissioningProxyPafTransport *>(context);
// Result cache, TTL and the combined MaxCachedResults cap all live in the cluster's
// scan cache; this just builds the kWiFiPAF result and reports it.
if (self != nullptr && self->mHost != nullptr)
{
self->mHost->ScanCache().Report(MakeScanResult(address, discriminator, vendorId, productId, extendedData, wiFiBand));
}
}
void CommissioningProxyPafTransport::HandleForegroundScanResult(void * context, ByteSpan address, uint16_t discriminator,
uint16_t vendorId, uint16_t productId, ByteSpan extendedData,
uint16_t wiFiBand)
{
auto * self = static_cast<CommissioningProxyPafTransport *>(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 kWiFiPAF 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, "Paf::Scan: result store full (%u); dropping discriminator=%u",
static_cast<unsigned>(kMaxScanResults), discriminator);
return;
}
ScanRecord & r = self->mScanResults[self->mScanResultCount];
r = ScanRecord{};
// Copy the variable-length fields: the adapter's spans die when this returns, while
// the record lives until the scan completes.
if (!address.empty() && address.size() <= sizeof(r.address))
{
memcpy(r.address, address.data(), address.size());
r.addressLen = static_cast<uint8_t>(address.size());
}
if (!extendedData.empty() && extendedData.size() <= sizeof(r.extendedData))
{
memcpy(r.extendedData, extendedData.data(), extendedData.size());
r.extendedDataLen = static_cast<uint8_t>(extendedData.size());
}
else if (extendedData.size() > sizeof(r.extendedData))
{
// Dropped rather than truncated: a partial blob reported as complete would be
// worse than none. The device itself is still reported.
ChipLogProgress(AppServer, "Paf::Scan: discriminator=%u ExtendedData is %u bytes, max %u - omitting it", discriminator,
static_cast<unsigned>(extendedData.size()), static_cast<unsigned>(sizeof(r.extendedData)));
}
r.discriminator = discriminator;
r.vendorId = vendorId;
r.productId = productId;
r.wiFiBand = wiFiBand;
self->mScanResultCount++;
ChipLogProgress(AppServer, "Paf::Scan: discovered discriminator=%u vid=0x%04x pid=0x%04x band=%u", discriminator, vendorId,
productId, wiFiBand);
}
void CommissioningProxyPafTransport::HandleForegroundScanDone(void * context)
{
auto * self = static_cast<CommissioningProxyPafTransport *>(context);
VerifyOrReturn(self != nullptr);
self->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 (self->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>(self->mScanResultCount, self->mHost->GetMaxCachedResults());
ScanResultT out[kMaxScanResults];
for (size_t i = 0; i < count; i++)
{
const ScanRecord & d = self->mScanResults[i];
out[i] = MakeScanResult(ByteSpan(d.address, d.addressLen), d.discriminator, d.vendorId, d.productId,
ByteSpan(d.extendedData, d.extendedDataLen), d.wiFiBand);
}
self->mHost->ScanAggregator().Contribute(Span<const ScanResultT>(out, count));
}
self->mScanResultCount = 0;
// Resume a background scan that was paused to make room for the one-shot scan. The
// platform can hold the NAN subscribe slot until this returns, so the resume is deferred.
self->OnAllSessionsClosed();
}
// ==================================================================
// Connect
// ==================================================================
void CommissioningProxyPafTransport::RemovePafSessionAndCloseEndpoint(uint16_t discriminator)
{
WiFiPAF::WiFiPAFLayer & pafLayer = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer();
WiFiPAF::WiFiPAFSession key{};
key.discriminator = discriminator;
// Capture the session before RmPafSession clears the slot so we can close any PAFTP
// endpoint the handshake created; otherwise it leaks from the 2-slot pool until its
// own timer self-closes. CloseEndPoint is a no-op if there is none.
WiFiPAF::WiFiPAFSession endpointSession{};
bool haveEndpoint = false;
WiFiPAF::WiFiPAFSession * pInfo = pafLayer.GetPAFInfo(WiFiPAF::PafInfoAccess::kAccDisc, key);
if (pInfo != nullptr)
{
endpointSession = *pInfo;
haveEndpoint = true;
}
CHIP_ERROR rmErr = pafLayer.RmPafSession(WiFiPAF::PafInfoAccess::kAccDisc, key);
if (rmErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "RemovePafSessionAndCloseEndpoint: RmPafSession: %" CHIP_ERROR_FORMAT, rmErr.Format());
}
if (haveEndpoint)
{
pafLayer.CloseEndPoint(endpointSession);
}
}
void CommissioningProxyPafTransport::FailPendingConnect(Status status, bool cancelTimer)
{
// 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());
if (cancelTimer)
{
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();
// Null the platform connect callbacks before cancelling the subscribe so the
// cancel-connect event posted by the platform does not re-enter HandleConnectError.
CHIP_ERROR cancelIncompleteErr = DeviceLayer::ConnectivityMgr().WiFiPAFCancelIncompleteSubscribe();
if (cancelIncompleteErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "FailPendingConnect: WiFiPAFCancelIncompleteSubscribe: %" CHIP_ERROR_FORMAT,
cancelIncompleteErr.Format());
}
RemovePafSessionAndCloseEndpoint(ctx.discriminator);
if (ctx.subscribeId != 0)
{
CHIP_ERROR cancelErr = DeviceLayer::ConnectivityMgr().WiFiPAFCancelSubscribe(ctx.subscribeId);
if (cancelErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "FailPendingConnect: WiFiPAFCancelSubscribe(%u): %" CHIP_ERROR_FORMAT, ctx.subscribeId,
cancelErr.Format());
}
}
if (app::CommandHandler * cmd = ctx.handle.Get())
{
cmd->AddStatus(ctx.path, status);
}
// The connect freed the NAN subscribe slot; resume a background scan that was
// deferred or paused for it (a failed connect registers no session, so the cluster's
// OnAllSessionsClosed path does not run).
OnAllSessionsClosed();
}
void CommissioningProxyPafTransport::OnConnectTimeout()
{
if (!mPendingConnect.has_value())
{
return; // Success or error callback already fired first; nothing to do.
}
ChipLogError(AppServer, "ProxyConnectRequest: timeout waiting for WiFiPAF connect (disc %u)", mPendingConnect->discriminator);
FailPendingConnect(Status::Timeout, /*cancelTimer=*/false);
}
void CommissioningProxyPafTransport::HandleConnectSuccess(void * context)
{
auto * self = static_cast<CommissioningProxyPafTransport *>(context);
VerifyOrReturn(self != nullptr);
if (!self->mPendingConnect.has_value())
{
ChipLogProgress(AppServer, "OnConnectSuccess: no pending connect ctx; ignoring stale callback");
return;
}
self->mTimerDelegate.CancelTimer(&self->mConnectTimer);
const uint16_t discriminator = self->mPendingConnect->discriminator;
WiFiPAF::WiFiPAFSession keyInfo{};
keyInfo.discriminator = discriminator;
WiFiPAF::WiFiPAFLayer & layer = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer();
WiFiPAF::WiFiPAFSession * pPafInfo = layer.GetPAFInfo(WiFiPAF::PafInfoAccess::kAccDisc, keyInfo);
app::CommandHandler * cmd = self->mPendingConnect->handle.Get();
if (cmd == nullptr || pPafInfo == nullptr)
{
ChipLogError(AppServer, "OnConnectSuccess: cmd=%p pPafInfo=%p for disc %u", (void *) cmd, (void *) pPafInfo, discriminator);
// The connect completed but the session cannot be handed to the commissioner
// (originating exchange gone, or the PAF session vanished). Run the full teardown
// so an established-but-untracked session does not leak its PAF pool slot, PAFTP
// endpoint and NAN subscribe. The timeout timer was already cancelled above.
self->FailPendingConnect(Status::Failure, /*cancelTimer=*/false);
return;
}
if (self->mPendingConnect->cluster == nullptr)
{
ChipLogError(AppServer, "OnConnectSuccess: cluster gone at connect complete; tearing down PAF session");
self->FailPendingConnect(Status::Failure, /*cancelTimer=*/false);
return;
}
SessionSlot * slot = self->ClaimSlot();
if (slot == nullptr)
{
// The cluster's MaxSessions gate should have rejected the ProxyConnectRequest
// before it reached the driver, so this means the two disagree.
ChipLogError(AppServer, "OnConnectSuccess: no free session slot; tearing down PAF session");
self->FailPendingConnect(Status::ResourceExhausted, /*cancelTimer=*/false);
return;
}
ConnectCtx ctx = std::move(*self->mPendingConnect);
self->mPendingConnect.reset();
const uint16_t sessionId = ctx.cluster->Sessions().AllocSessionId();
slot->inUse = true;
slot->sessionId = sessionId;
slot->pafSession = *pPafInfo;
ctx.cluster->Sessions().RegisterSession(sessionId, CapabilitiesBitmap::kWiFiPAF, ctx.fabricIndex);
ChipLogProgress(AppServer, "ProxyConnectRequest: WiFiPAF connected, proxy session %u (disc %u peer_id %u)", sessionId,
ctx.discriminator, pPafInfo->peer_id);
CHIP_ERROR stateErr = ctx.cluster->SetCPState(CommissioningProxyCluster::kState_CPConnected);
if (stateErr != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "OnConnectSuccess: SetCPState failed: %" CHIP_ERROR_FORMAT, stateErr.Format());
}
Commands::ProxyConnectResponse::Type response;
response.sessionID = sessionId;
cmd->AddResponse(ctx.path, response);
}
void CommissioningProxyPafTransport::HandleConnectError(void * context, CHIP_ERROR err)
{
auto * self = static_cast<CommissioningProxyPafTransport *>(context);
VerifyOrReturn(self != nullptr);
if (!self->mPendingConnect.has_value())
{
ChipLogProgress(AppServer,
"OnConnectError: ignoring stale callback after successful connect "
"(err: %" CHIP_ERROR_FORMAT ")",
err.Format());
return;
}
ChipLogError(AppServer, "ProxyConnectRequest: WiFiPAF connect failed: %" CHIP_ERROR_FORMAT, err.Format());
self->FailPendingConnect(Status::Failure, /*cancelTimer=*/true);
}
// ==================================================================
// CommissioningProxyTransport
// ==================================================================
void CommissioningProxyPafTransport::SetHost(CommissioningProxyCluster * host)
{
mHost = host;
}
Status CommissioningProxyPafTransport::Connect(app::CommandHandler * commandObj, const DataModel::InvokeRequest & request,
uint16_t discriminator, System::Clock::Seconds16 timeout)
{
// Only one PAF connect can be in flight at a time. Reject a second one up front,
// before pausing the background scan or touching the PAF session pool, so a refused
// connect leaves no side effects behind.
if (mPendingConnect.has_value())
{
ChipLogError(AppServer, "ProxyConnectRequest: a PAF connect is already in progress");
return Status::Busy;
}
// Pause any background scan so the NAN subscribe slot can be reused for the PAF
// connect subscribe; the registry resumes it on OnAllSessionsClosed().
mBgScan.Pause();
// The WiFiPAF layer is initialized once by the platform ConnectivityManager at
// startup. Do NOT call Init() here: it memset()s the shared endpoint pool, which
// would wipe a live PAFTP session while leaving its scheduled ack/retransmit timers
// pointing at the zeroed slot.
// Install the proxy receive delegate so messages arriving from the commissionee over
// WiFiPAF are forwarded to the commissioner as ProxyMessageResponse rather than being
// injected into the application's own Matter transport stack.
mProxyDelegate.Install();
// Register a PAF session so the platform's discovery result can locate the right
// entry by discriminator.
WiFiPAF::WiFiPAFSession pafSessionInfo{};
pafSessionInfo.role = WiFiPAF::kWiFiPafRole_Subscriber;
pafSessionInfo.discriminator = discriminator;
CHIP_ERROR addErr = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().AddPafSession(WiFiPAF::PafInfoAccess::kAccDisc, pafSessionInfo);
if (addErr != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyConnectRequest: AddPafSession failed: %" CHIP_ERROR_FORMAT, addErr.Format());
OnAllSessionsClosed();
return (addErr == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) ? Status::ResourceExhausted : Status::Failure;
}
// 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.subscribeId = 0;
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);
}
CHIP_ERROR err = DeviceLayer::ConnectivityMgr().WiFiPAFSubscribe(discriminator, this, HandleConnectSuccess, HandleConnectError);
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyConnectRequest: WiFiPAFSubscribe failed: %" CHIP_ERROR_FORMAT, err.Format());
CHIP_ERROR rmErr = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().RmPafSession(WiFiPAF::PafInfoAccess::kAccDisc, pafSessionInfo);
if (rmErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "ProxyConnectRequest cleanup: RmPafSession: %" CHIP_ERROR_FORMAT, rmErr.Format());
}
mPendingConnect.reset();
OnAllSessionsClosed();
return Status::Failure;
}
// WiFiPAFSubscribe does not return the id the platform assigned, so recover it from
// the adapter; the transport needs it to cancel this specific subscribe.
mPendingConnect->subscribeId = mAdapter.PendingConnectSubscribeId();
if (hasTimeout)
{
CHIP_ERROR timerErr = mTimerDelegate.StartTimer(&mConnectTimer, timeout);
if (timerErr != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "ProxyConnectRequest: StartTimer failed: %" CHIP_ERROR_FORMAT, timerErr.Format());
CHIP_ERROR rmErr2 =
WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().RmPafSession(WiFiPAF::PafInfoAccess::kAccDisc, pafSessionInfo);
if (rmErr2 != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "ProxyConnectRequest cleanup: RmPafSession: %" CHIP_ERROR_FORMAT, rmErr2.Format());
}
if (mPendingConnect->subscribeId != 0)
{
(void) DeviceLayer::ConnectivityMgr().WiFiPAFCancelSubscribe(mPendingConnect->subscribeId);
}
mPendingConnect.reset();
OnAllSessionsClosed();
return Status::Failure;
}
}
ChipLogProgress(AppServer, "ProxyConnectRequest: WiFiPAFSubscribe started for discriminator %u (subscribe_id %u)",
discriminator, mPendingConnect->subscribeId);
return Status::Success;
}
Status CommissioningProxyPafTransport::CancelPendingConnect(FabricIndex fabricIndex)
{
if (!mPendingConnect.has_value())
{
return Status::InvalidInState;
}
if (fabricIndex != mPendingConnect->fabricIndex)
{
ChipLogProgress(AppServer, "CancelPendingConnect: pending PAF connect owned by fabric %u, rejected fabric %u",
mPendingConnect->fabricIndex, fabricIndex);
return Status::NotFound;
}
ChipLogProgress(AppServer, "CancelPendingConnect: cancelling pending PAF connect for fabric %u", fabricIndex);
FailPendingConnect(Status::Failure, /*cancelTimer=*/true);
return Status::Success;
}
Status CommissioningProxyPafTransport::Disconnect(uint16_t sessionId)
{
SessionSlot * slot = FindSlot(sessionId);
if (slot == nullptr)
{
return Status::NotFound;
}
WiFiPAF::WiFiPAFSession pafSession = slot->pafSession;
*slot = SessionSlot{};
CHIP_ERROR rmErr = WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().RmPafSession(WiFiPAF::PafInfoAccess::kAccSessionId, pafSession);
if (rmErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "ProxyDisconnectRequest: RmPafSession for session %u: %" CHIP_ERROR_FORMAT, sessionId,
rmErr.Format());
}
// Explicitly close the PAFTP endpoint so its ack-received timer does not fire 30 s
// later.
WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().CloseEndPoint(pafSession);
// Terminate the NAN subscribe instance that backed this session. For a subscriber
// session the WiFiPAFSession id IS the wpa_supplicant subscribe_id, so cancelling it
// tears down the PAFTP session. Without this the subscribe is left registered in the
// long-lived wpa_supplicant and keeps firing discovery callbacks for the rest of the
// proxy's lifetime.
if (pafSession.id != 0)
{
CHIP_ERROR cancelErr = DeviceLayer::ConnectivityMgr().WiFiPAFCancelSubscribe(pafSession.id);
if (cancelErr != CHIP_NO_ERROR)
{
ChipLogDetail(AppServer, "ProxyDisconnectRequest: WiFiPAFCancelSubscribe(%u): %" CHIP_ERROR_FORMAT, pafSession.id,
cancelErr.Format());
}
}
return Status::Success;
}
CHIP_ERROR CommissioningProxyPafTransport::SendMessage(uint16_t sessionId, System::PacketBufferHandle && buf)
{
SessionSlot * slot = FindSlot(sessionId);
if (slot == nullptr)
{
return CHIP_ERROR_KEY_NOT_FOUND;
}
// MUST use WiFiPAFLayer::SendMessage (not the platform's raw WiFiPAFSend):
// WiFiPAFSend is the low-level NAN transmit used internally by PAFTP; calling it
// directly bypasses PAFTP framing and the end device cannot parse the frame.
// SendMessage goes through the PAFTP endpoint, which fragments and frames the
// message before calling WiFiPAFSend.
return WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().SendMessage(slot->pafSession, std::move(buf));
}
Status CommissioningProxyPafTransport::Scan(System::Clock::Seconds16 scanMaxTime)
{
if (mScanInProgress)
{
ChipLogProgress(AppServer, "ProxyScanRequest: PAF scan already in progress — returning Busy");
return Status::Busy;
}
// The one-shot scan and the background scan share the single NAN subscribe slot.
// Pause any running background scan so the scan below does not fail with BUSY; the
// scan-done callback resumes it when the foreground scan completes.
mBgScan.Pause();
mScanResultCount = 0;
mScanInProgress = true;
// The platform owns the scan window here, so unlike BLE there is no timer to arm:
// HandleForegroundScanDone closes the scan out.
CHIP_ERROR err = mAdapter.StartForegroundScan(scanMaxTime, HandleForegroundScanResult, HandleForegroundScanDone, this);
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "Paf::Scan: StartForegroundScan failed: %" CHIP_ERROR_FORMAT, err.Format());
mScanInProgress = false;
OnAllSessionsClosed();
return Status::Failure;
}
ChipLogProgress(AppServer, "Paf::Scan: started, scanMaxTime=%us", scanMaxTime.count());
return Status::Success;
}
Status CommissioningProxyPafTransport::BgScanStart(System::Clock::Seconds16 timeout, BitMask<WiFiBandBitmap> wiFiBands,
FabricIndex fabricIndex, NodeId nodeId)
{
// This driver services the kWiFiPAF transport. The registry starts or defers the
// hardware scan (StartHardwareScan reports BUSY while a ProxyConnect holds the single
// NAN subscribe slot) and records the requested bands per fabric.
return mBgScan.Start(fabricIndex, nodeId, BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kWiFiPAF), wiFiBands, timeout);
}
Status CommissioningProxyPafTransport::BgScanStop(BitMask<CapabilitiesBitmap> transport, BitMask<WiFiBandBitmap> wiFiBands,
FabricIndex fabricIndex, NodeId nodeId)
{
// The registry applies the spec transport/band overlap semantics (NOT_FOUND for an
// unknown fabric, SUCCESS for a non-overlapping request, keep-scanning while any
// fabric still covers the transport, stop+clear on the last fabric).
return mBgScan.Stop(fabricIndex, nodeId, transport, wiFiBands);
}
void CommissioningProxyPafTransport::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 CommissioningProxyPafTransport::OnAllSessionsClosed()
{
if (mBgScan.IsPaused() && !mBgScan.IsEmpty())
{
// Zero delay: still dispatched from the event loop rather than inline, but
// cancellable in Shutdown() so it cannot fire against a destroyed transport. The
// registry re-checks whether a connect is now pending and stays paused if so.
mTimerDelegate.CancelTimer(&mResumeBgScanTimer);
CHIP_ERROR err = mTimerDelegate.StartTimer(&mResumeBgScanTimer, System::Clock::Timeout(0));
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "OnAllSessionsClosed: could not defer bg-scan resume: %" CHIP_ERROR_FORMAT, err.Format());
}
}
}
bool CommissioningProxyPafTransport::IsConnectPending() const
{
return mPendingConnect.has_value();
}
void CommissioningProxyPafTransport::Shutdown()
{
// Close any active PAF sessions: fail in-flight messages, close PAFTP endpoints,
// cancel NAN subscribes, and remove them from the cluster's session manager. Take the
// sessions out first so the slots are free if a close fires a callback synchronously.
WiFiPAF::WiFiPAFSession closing[kMaxSessions] = {};
uint16_t closingIds[kMaxSessions] = {};
size_t closingCount = 0;
for (auto & slot : mSessions)
{
if (slot.inUse)
{
closingIds[closingCount] = slot.sessionId;
closing[closingCount] = slot.pafSession;
closingCount++;
}
slot = SessionSlot{};
}
for (size_t i = 0; i < closingCount; i++)
{
if (mHost != nullptr)
{
mHost->Sessions().DispatchMessageFailure(closingIds[i], Status::Failure);
mHost->Sessions().RemoveSession(closingIds[i]);
}
LogErrorOnFailure(WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().RmPafSession(WiFiPAF::PafInfoAccess::kAccSessionId, closing[i]));
WiFiPAF::WiFiPAFLayer::GetWiFiPAFLayer().CloseEndPoint(closing[i]);
if (closing[i].id != 0)
{
(void) DeviceLayer::ConnectivityMgr().WiFiPAFCancelSubscribe(closing[i].id);
}
}
// Detach an in-flight foreground scan so its completion cannot reach this object
// after it is gone; the platform may keep scanning, but nothing is reported.
mAdapter.StopForegroundScan();
mScanInProgress = false;
mScanResultCount = 0;
// Drop the deferred bg-scan resume, or it fires against a destroyed transport.
mTimerDelegate.CancelTimer(&mResumeBgScanTimer);
// Tear down an in-flight ProxyConnect: cancel its timeout timer and the NAN
// subscribe, and drop the PAF session, so nothing outlives the cluster. Do not call
// FailPendingConnect() here — it would invoke OnAllSessionsClosed(), which would
// schedule a scan restart just as the cleanup below stops everything.
if (mPendingConnect.has_value())
{
mTimerDelegate.CancelTimer(&mConnectTimer);
ConnectCtx ctx = std::move(*mPendingConnect);
mPendingConnect.reset();
(void) DeviceLayer::ConnectivityMgr().WiFiPAFCancelIncompleteSubscribe();
if (ctx.subscribeId != 0)
{
(void) DeviceLayer::ConnectivityMgr().WiFiPAFCancelSubscribe(ctx.subscribeId);
}
RemovePafSessionAndCloseEndpoint(ctx.discriminator);
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 WiFiPAFLayer::mWiFiPAFTransport so a later PAF 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.
mHost = nullptr;
}
} // namespace CommissioningProxy
} // namespace Clusters
} // namespace app
} // namespace chip