blob: e5d0ba99b4cd1fb4702d351ddf83436add72f377 [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 "CommissioningProxyBgScanRegistry.h"
#include <lib/support/logging/CHIPLogging.h>
#include <system/SystemClock.h>
#include <algorithm>
#include <chrono>
namespace chip {
namespace app {
namespace Clusters {
namespace CommissioningProxy {
using Protocols::InteractionModel::Status;
namespace {
// Timestamp subtraction is 64-bit; StartTimer takes the 32-bit Timeout.
System::Clock::Timeout RemainingUntil(System::Clock::Timestamp deadline, System::Clock::Timestamp now)
{
return (deadline > now) ? std::chrono::duration_cast<System::Clock::Timeout>(deadline - now) : System::Clock::Timeout(0);
}
} // namespace
size_t CommissioningProxyBgScanRegistry::FabricState::RequestCount() const
{
size_t count = 0;
for (const auto & slot : requests)
{
if (slot.inUse)
{
count++;
}
}
return count;
}
CommissioningProxyBgScanRegistry::RequestSlot * CommissioningProxyBgScanRegistry::FabricState::Find(NodeId nodeId)
{
for (auto & slot : requests)
{
if (slot.inUse && slot.nodeId == nodeId)
{
return &slot;
}
}
return nullptr;
}
CommissioningProxyBgScanRegistry::RequestSlot * CommissioningProxyBgScanRegistry::FabricState::FindFree()
{
for (auto & slot : requests)
{
if (!slot.inUse)
{
return &slot;
}
}
return nullptr;
}
CommissioningProxyBgScanRegistry::FabricState * CommissioningProxyBgScanRegistry::FindFabric(FabricIndex fabricIndex)
{
for (auto & fabric : mFabrics)
{
if (fabric.inUse && fabric.fabricIndex == fabricIndex)
{
return &fabric;
}
}
return nullptr;
}
bool CommissioningProxyBgScanRegistry::AnyFabricInUse() const
{
for (const auto & fabric : mFabrics)
{
if (fabric.inUse)
{
return true;
}
}
return false;
}
void CommissioningProxyBgScanRegistry::CancelLifetime(FabricState & state)
{
if (state.lifetime.armed)
{
mTimerDelegate.CancelTimer(&state.lifetime);
state.lifetime.armed = false;
}
}
void CommissioningProxyBgScanRegistry::OnBecameEmpty()
{
// Stop the radio only if this registry currently owns it; if paused, a connect or
// foreground scan owns it and must not be disturbed. The cached results are dropped
// in either case — no fabric wants them refreshed any more.
if (!mPaused)
{
mHardware.StopHardwareScan();
}
mPaused = false;
mHardware.ClearCachedResults({}); // the transport stopped, so all of its results go
}
BitMask<WiFiBandBitmap> CommissioningProxyBgScanRegistry::BandsInUse() const
{
uint16_t bits = 0;
for (const auto & fabric : mFabrics)
{
if (!fabric.inUse)
{
continue;
}
for (const auto & slot : fabric.requests)
{
if (slot.inUse)
{
bits = static_cast<uint16_t>(bits | slot.request.wiFiBands.Raw());
}
}
}
return BitMask<WiFiBandBitmap>(bits);
}
void CommissioningProxyBgScanRegistry::ClearBandsNoLongerScanned(BitMask<WiFiBandBitmap> candidates)
{
// Spec: clear the results for the bands scanning has stopped on — which is only the
// bands no other request, on this or any other fabric, still asks for.
const uint16_t stopped = static_cast<uint16_t>(candidates.Raw() & ~BandsInUse().Raw());
if (stopped != 0)
{
mHardware.ClearCachedResults(BitMask<WiFiBandBitmap>(stopped));
}
}
bool CommissioningProxyBgScanRegistry::LatestDeadline(const FabricState & state, System::Clock::Timestamp & out)
{
System::Clock::Timestamp latest{ 0 };
bool any = false;
for (const auto & slot : state.requests)
{
if (!slot.inUse)
{
continue;
}
if (!slot.request.hasTimeout)
{
return false; // one request never expires, so the fabric never does
}
latest = std::max(latest, slot.request.expiresAt);
any = true;
}
out = latest;
return any;
}
BitMask<WiFiBandBitmap> CommissioningProxyBgScanRegistry::ReleaseFabric(FabricState & state)
{
uint16_t releasedBands = 0;
for (auto & slot : state.requests)
{
if (slot.inUse)
{
releasedBands = static_cast<uint16_t>(releasedBands | slot.request.wiFiBands.Raw());
slot.inUse = false;
}
}
CancelLifetime(state);
state.inUse = false;
return BitMask<WiFiBandBitmap>(releasedBands);
}
BitMask<WiFiBandBitmap> CommissioningProxyBgScanRegistry::RecomputeFabricLifetime(FabricIndex fabricIndex, FabricState & state)
{
CancelLifetime(state);
System::Clock::Timestamp deadline{ 0 };
if (!LatestDeadline(state, deadline))
{
return {};
}
state.lifetime.registry = this;
state.lifetime.fabricIndex = fabricIndex;
if (mTimerDelegate.StartTimer(&state.lifetime, RemainingUntil(deadline, mTimerDelegate.GetCurrentMonotonicTimestamp())) !=
CHIP_NO_ERROR)
{
// The fabric would otherwise scan unbounded. Nothing here can reject the command
// that got us here (it has already been applied), so drop the fabric instead and
// let the caller settle the radio and the cached results.
ChipLogError(AppServer, "BgScan: could not re-arm lifetime for fabricIndex=%u; dropping it", fabricIndex);
return ReleaseFabric(state);
}
state.lifetime.armed = true;
return {};
}
Status CommissioningProxyBgScanRegistry::Start(FabricIndex fabricIndex, NodeId nodeId, BitMask<CapabilitiesBitmap> transport,
BitMask<WiFiBandBitmap> wiFiBands, System::Clock::Seconds16 timeout)
{
const bool wasEmpty = !AnyFabricInUse();
// Reject before any side effect: a fabric may only hold so many concurrent requests,
// and a node already holding one is refreshing rather than adding.
FabricState * fabric = FindFabric(fabricIndex);
if (fabric != nullptr && fabric->Find(nodeId) == nullptr && fabric->FindFree() == nullptr)
{
ChipLogError(AppServer, "BgScan: fabricIndex=%u already has %u background scans", fabricIndex,
static_cast<unsigned>(fabric->RequestCount()));
return Status::ResourceExhausted;
}
if (fabric == nullptr)
{
for (auto & candidate : mFabrics)
{
if (!candidate.inUse)
{
fabric = &candidate;
break;
}
}
VerifyOrReturnError(fabric != nullptr, Status::ResourceExhausted);
}
// Start (or resume) the hardware scan on the first fabric, or whenever we are
// paused/deferred. BUSY means the radio is held elsewhere; register the fabric
// anyway and stay paused so ResumeIfNeeded() restarts it once it frees up.
if (wasEmpty || mPaused)
{
CHIP_ERROR err = mHardware.StartHardwareScan();
if (err == CHIP_ERROR_BUSY)
{
mPaused = true;
ChipLogProgress(AppServer, "BgScan: radio busy, deferring hardware scan (will resume when free)");
}
else if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "BgScan: StartHardwareScan failed: %" CHIP_ERROR_FORMAT, err.Format());
return Status::Failure;
}
else
{
mPaused = false;
}
}
// Work out the fabric's deadline as it will be once this request is applied, so the
// timer can be armed before anything is committed.
const auto now = mTimerDelegate.GetCurrentMonotonicTimestamp();
Request incoming;
incoming.transport = transport;
incoming.wiFiBands = wiFiBands;
incoming.hasTimeout = (timeout.count() > 0);
incoming.expiresAt = now + timeout;
bool needTimer = incoming.hasTimeout;
System::Clock::Timestamp deadline = incoming.expiresAt;
if (fabric->inUse)
{
for (const auto & slot : fabric->requests)
{
if (!slot.inUse || slot.nodeId == nodeId)
{
continue; // this node's request is about to be replaced
}
if (!slot.request.hasTimeout)
{
needTimer = false;
break;
}
deadline = std::max(deadline, slot.request.expiresAt);
}
}
// Arming can fail, so it happens before the table is touched. The old timer is
// cancelled only once the new one is running.
const bool hadTimer = fabric->inUse && fabric->lifetime.armed;
if (needTimer)
{
// Cancel first: a context can hold only one timer, and this reuses the slot's.
if (hadTimer)
{
mTimerDelegate.CancelTimer(&fabric->lifetime);
}
fabric->lifetime.registry = this;
fabric->lifetime.fabricIndex = fabricIndex;
CHIP_ERROR timerErr = mTimerDelegate.StartTimer(&fabric->lifetime, RemainingUntil(deadline, now));
if (timerErr != CHIP_NO_ERROR)
{
// Without a lifetime timer the hardware scan would run unbounded, so reject.
// Any requests the fabric already held go with it: the old timer was cancelled
// just above and cannot be put back, so keeping them would scan unbounded too.
// If this call is what started the radio and nothing is left to keep it
// running, undo that as well.
ChipLogError(AppServer, "BgScan: lifetime StartTimer failed: %" CHIP_ERROR_FORMAT, timerErr.Format());
fabric->lifetime.armed = false;
const BitMask<WiFiBandBitmap> releasedBands = ReleaseFabric(*fabric);
if (!AnyFabricInUse())
{
OnBecameEmpty();
}
else
{
ClearBandsNoLongerScanned(releasedBands);
}
return Status::Failure;
}
fabric->lifetime.armed = true;
}
else if (hadTimer)
{
CancelLifetime(*fabric);
}
// From here nothing can fail: the hardware scan is running and the fabric's new
// timer is armed, so the table is safe to modify.
//
// Each node on a fabric keeps its own request and the fabric scans the union of
// them, so this adds a request or replaces only this node's previous one.
fabric->inUse = true;
fabric->fabricIndex = fabricIndex;
RequestSlot * slot = fabric->Find(nodeId);
if (slot == nullptr)
{
slot = fabric->FindFree();
VerifyOrDie(slot != nullptr); // capacity was checked above
}
slot->inUse = true;
slot->nodeId = nodeId;
slot->request = incoming;
return Status::Success;
}
Status CommissioningProxyBgScanRegistry::Stop(FabricIndex fabricIndex, NodeId nodeId, BitMask<CapabilitiesBitmap> transport,
BitMask<WiFiBandBitmap> wiFiBands)
{
FabricState * fabric = FindFabric(fabricIndex);
VerifyOrReturnValue(fabric != nullptr, Status::NotFound);
// Spec: if the client's NodeID and FabricID do not match those recorded when the
// scan was started, take no action and reject with NOT_FOUND.
RequestSlot * slot = fabric->Find(nodeId);
VerifyOrReturnValue(slot != nullptr, Status::NotFound);
const uint8_t reqTransportBits = transport.Raw();
const uint16_t reqBandBits = wiFiBands.Raw();
const uint8_t ownTransportBits = slot->request.transport.Raw();
const uint16_t ownBandBits = slot->request.wiFiBands.Raw();
// A transport bitmap of zero means "stop only the given bands" (spec); otherwise
// stop the intersection of the requested and this node's own transports/bands.
const uint8_t stopTransportBits = (reqTransportBits == 0) ? 0 : static_cast<uint8_t>(reqTransportBits & ownTransportBits);
const uint16_t stopBandBits = static_cast<uint16_t>(reqBandBits & ownBandBits);
if (stopTransportBits == 0 && stopBandBits == 0)
{
// Nothing the caller asked to stop was actually being scanned for this node.
return Status::Success;
}
const uint8_t remainTransport = static_cast<uint8_t>(ownTransportBits & ~stopTransportBits);
const uint16_t remainBands = static_cast<uint16_t>(ownBandBits & ~stopBandBits);
// A request holds one transport bit (see Start), so the first term decides whenever
// the stop names this transport; remainBands only fires on a band-only stop that
// clears the last band. A request with no bands at all is not constrained by them.
if (remainTransport == 0 || (ownBandBits != 0 && remainBands == 0))
{
slot->inUse = false;
}
else
{
slot->request.transport = BitMask<CapabilitiesBitmap>(remainTransport);
slot->request.wiFiBands = BitMask<WiFiBandBitmap>(remainBands);
}
// Dropping a request can shorten the fabric back to a surviving request's deadline,
// so the timer is recomputed rather than left where the removed request put it. A
// timer that cannot be re-armed releases the fabric, whose bands then go too.
uint16_t releasedBands = stopBandBits;
if (fabric->RequestCount() == 0)
{
ReleaseFabric(*fabric);
}
else
{
releasedBands = static_cast<uint16_t>(releasedBands | RecomputeFabricLifetime(fabricIndex, *fabric).Raw());
}
// Other requests — on this fabric or another — may still want the radio, so it is
// only torn down once nothing is left. Short of that, results for bands nobody
// scans any more are still dropped.
if (!AnyFabricInUse())
{
OnBecameEmpty();
}
else
{
ClearBandsNoLongerScanned(BitMask<WiFiBandBitmap>(releasedBands));
}
return Status::Success;
}
void CommissioningProxyBgScanRegistry::RemoveFabric(FabricIndex fabricIndex)
{
FabricState * fabric = FindFabric(fabricIndex);
VerifyOrReturn(fabric != nullptr);
const BitMask<WiFiBandBitmap> removedBands = ReleaseFabric(*fabric);
ChipLogProgress(AppServer, "BgScan: dropped background scans for removed fabricIndex=%u", fabricIndex);
if (!AnyFabricInUse())
{
OnBecameEmpty();
}
else
{
ClearBandsNoLongerScanned(removedBands);
}
}
void CommissioningProxyBgScanRegistry::Pause()
{
if (AnyFabricInUse() && !mPaused)
{
mHardware.StopHardwareScan();
mPaused = true;
ChipLogProgress(AppServer, "BgScan: paused (radio needed for connect/foreground scan)");
}
}
void CommissioningProxyBgScanRegistry::ResumeIfNeeded()
{
if (!mPaused || !AnyFabricInUse())
{
return;
}
CHIP_ERROR err = mHardware.StartHardwareScan();
if (err == CHIP_NO_ERROR)
{
mPaused = false;
ChipLogProgress(AppServer, "BgScan: resumed");
}
else if (err == CHIP_ERROR_BUSY)
{
// Radio still held; stay paused and retry on the next ResumeIfNeeded().
}
else
{
ChipLogError(AppServer, "BgScan: resume failed: %" CHIP_ERROR_FORMAT, err.Format());
}
}
void CommissioningProxyBgScanRegistry::Shutdown()
{
const bool hadFabrics = AnyFabricInUse();
for (auto & fabric : mFabrics)
{
ReleaseFabric(fabric);
}
if (hadFabrics && !mPaused)
{
mHardware.StopHardwareScan();
}
mPaused = false;
}
void CommissioningProxyBgScanRegistry::OnLifetimeExpiry(FabricIndex fabricIndex)
{
FabricState * fabric = FindFabric(fabricIndex);
VerifyOrReturn(fabric != nullptr);
// The fabric's timer sits at the latest deadline of all its requests, so when it
// fires every one of them has expired and the whole fabric goes. The timer has
// already been consumed, so just mark it disarmed (do not cancel the one that fired).
fabric->lifetime.armed = false;
// Spec: when the fabric's Timeout elapses its cached results are cleared — but only
// for bands no surviving fabric still scans.
const BitMask<WiFiBandBitmap> expiringBands = ReleaseFabric(*fabric);
ChipLogProgress(AppServer, "BgScan: lifetime expired for fabricIndex=%u", fabricIndex);
if (!AnyFabricInUse())
{
OnBecameEmpty();
}
else
{
ClearBandsNoLongerScanned(expiringBands);
}
}
} // namespace CommissioningProxy
} // namespace Clusters
} // namespace app
} // namespace chip