| /* | |
| * | |
| * 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/CommissioningProxyScanCache.h> | |
| #include <lib/support/logging/CHIPLogging.h> | |
| #include <algorithm> | |
| #include <cstring> | |
| #include <system/SystemClock.h> | |
| namespace chip { | |
| namespace app { | |
| namespace Clusters { | |
| namespace CommissioningProxy { | |
| namespace { | |
| // One periodic sweep (1 s granularity) reaps expired entries while the cache is | |
| // non-empty — far cheaper than a per-device TTL timer re-armed on every advert. | |
| constexpr System::Clock::Timeout kSweepInterval = System::Clock::Seconds16(1); | |
| } // namespace | |
| bool CommissioningProxyScanCache::Key::operator==(const Key & o) const | |
| { | |
| return transport == o.transport && discriminator == o.discriminator && vid == o.vid && pid == o.pid; | |
| } | |
| CommissioningProxyScanCache::Entry * CommissioningProxyScanCache::FindEntry(const Key & key) | |
| { | |
| for (auto & entry : mEntries) | |
| { | |
| if (entry.inUse && entry.key == key) | |
| { | |
| return &entry; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| void CommissioningProxyScanCache::ArmSweepIfNeeded() | |
| { | |
| if (mSweepArmed || Count() == 0) | |
| { | |
| return; | |
| } | |
| if (mTimerDelegate.StartTimer(this, kSweepInterval) == CHIP_NO_ERROR) | |
| { | |
| mSweepArmed = true; | |
| } | |
| } | |
| void CommissioningProxyScanCache::OnSweep() | |
| { | |
| mSweepArmed = false; | |
| auto now = mTimerDelegate.GetCurrentMonotonicTimestamp(); | |
| bool removed = false; | |
| for (auto & entry : mEntries) | |
| { | |
| if (entry.inUse && entry.expiresAt <= now) | |
| { | |
| ChipLogProgress(Zcl, "CommissioningProxyScanCache: TTL expired for discriminator %u (transport 0x%x)", | |
| entry.key.discriminator, entry.key.transport); | |
| entry.inUse = false; | |
| removed = true; | |
| } | |
| } | |
| if (removed) | |
| { | |
| mCluster.MarkCachedResultsDirty(); | |
| } | |
| ArmSweepIfNeeded(); | |
| } | |
| void CommissioningProxyScanCache::Report(const ScanResultEntry & result) | |
| { | |
| Key key{ static_cast<uint8_t>(result.transport.Raw()), result.discriminator, static_cast<uint16_t>(result.vendorID), | |
| result.productID }; | |
| auto expiresAt = mTimerDelegate.GetCurrentMonotonicTimestamp() + System::Clock::Seconds16(mCluster.GetCacheTimeout()); | |
| if (Entry * existing = FindEntry(key)) | |
| { | |
| // Re-discovery: refresh TTL only (the visible result is unchanged, so no dirty). | |
| existing->expiresAt = expiresAt; | |
| ArmSweepIfNeeded(); | |
| return; | |
| } | |
| Entry * slot = nullptr; | |
| for (auto & candidate : mEntries) | |
| { | |
| if (!candidate.inUse) | |
| { | |
| slot = &candidate; | |
| break; | |
| } | |
| } | |
| if (slot == nullptr) | |
| { | |
| ChipLogDetail(Zcl, "CommissioningProxyScanCache: full (%u entries), dropping discriminator %u", | |
| mCluster.GetMaxCachedResults(), key.discriminator); | |
| return; | |
| } | |
| *slot = Entry{}; | |
| slot->inUse = true; | |
| slot->key = key; | |
| slot->transport = result.transport; | |
| if (!result.address.IsNull()) | |
| { | |
| auto span = result.address.Value(); | |
| const size_t copyLen = std::min(span.size(), kMaxAddressBytes); | |
| memcpy(slot->address, span.data(), copyLen); | |
| slot->addressLen = static_cast<uint8_t>(copyLen); | |
| slot->hasAddress = true; | |
| } | |
| slot->discriminator = result.discriminator; | |
| slot->vendorID = result.vendorID; | |
| slot->productID = result.productID; | |
| if (!result.extendedData.IsNull()) | |
| { | |
| auto span = result.extendedData.Value(); | |
| const size_t copyLen = std::min(span.size(), kMaxExtendedDataBytes); | |
| memcpy(slot->extendedData, span.data(), copyLen); | |
| slot->extendedDataLen = static_cast<uint8_t>(copyLen); | |
| slot->hasExtendedData = true; | |
| } | |
| slot->wiFiBand = result.wiFiBand; | |
| slot->expiresAt = expiresAt; | |
| ChipLogProgress(Zcl, "CommissioningProxyScanCache: cached discriminator %u (transport 0x%x, total=%u)", key.discriminator, | |
| key.transport, Count()); | |
| mCluster.MarkCachedResultsDirty(); | |
| ArmSweepIfNeeded(); | |
| } | |
| void CommissioningProxyScanCache::ClearTransport(BitMask<CapabilitiesBitmap> transport, BitMask<WiFiBandBitmap> bands) | |
| { | |
| // Only PAFTP results carry a band, so a BLE stop always arrives with bands == 0. | |
| const BitMask<WiFiBandBitmap> kBandFallback{ WiFiBandBitmap::k2g4 }; | |
| bool removed = false; | |
| for (auto & entry : mEntries) | |
| { | |
| if (!entry.inUse) | |
| { | |
| continue; | |
| } | |
| const bool bandMatches = bands.Raw() == 0 || (entry.wiFiBand.ValueOr(kBandFallback).Raw() & bands.Raw()) != 0; | |
| if ((entry.key.transport & transport.Raw()) != 0 && bandMatches) | |
| { | |
| entry.inUse = false; | |
| removed = true; | |
| } | |
| } | |
| if (removed) | |
| { | |
| mCluster.MarkCachedResultsDirty(); | |
| } | |
| } | |
| uint8_t CommissioningProxyScanCache::Count() const | |
| { | |
| uint8_t count = 0; | |
| for (const auto & entry : mEntries) | |
| { | |
| count = static_cast<uint8_t>(count + (entry.inUse ? 1 : 0)); | |
| } | |
| return count; | |
| } | |
| CHIP_ERROR CommissioningProxyScanCache::Encode(AttributeValueEncoder & encoder) const | |
| { | |
| if (Count() == 0) | |
| { | |
| DataModel::Nullable<DataModel::List<const ScanResultEntry>> nullValue; | |
| return encoder.Encode(nullValue); | |
| } | |
| return encoder.EncodeList([this](const auto & listEncoder) -> CHIP_ERROR { | |
| for (const auto & e : mEntries) | |
| { | |
| if (!e.inUse) | |
| { | |
| continue; | |
| } | |
| ScanResultEntry r{}; | |
| if (e.hasAddress) | |
| { | |
| r.address.SetNonNull(ByteSpan(e.address, e.addressLen)); | |
| } | |
| else | |
| { | |
| r.address.SetNull(); | |
| } | |
| r.transport = e.transport; | |
| r.discriminator = e.discriminator; | |
| r.vendorID = e.vendorID; | |
| r.productID = e.productID; | |
| if (e.hasExtendedData) | |
| { | |
| r.extendedData.SetNonNull(ByteSpan(e.extendedData, e.extendedDataLen)); | |
| } | |
| else | |
| { | |
| r.extendedData.SetNull(); | |
| } | |
| r.wiFiBand = e.wiFiBand; | |
| ReturnErrorOnFailure(listEncoder.Encode(r)); | |
| } | |
| return CHIP_NO_ERROR; | |
| }); | |
| } | |
| void CommissioningProxyScanCache::Shutdown() | |
| { | |
| if (mSweepArmed) | |
| { | |
| mTimerDelegate.CancelTimer(this); | |
| mSweepArmed = false; | |
| } | |
| for (auto & entry : mEntries) | |
| { | |
| entry.inUse = false; | |
| } | |
| } | |
| } // namespace CommissioningProxy | |
| } // namespace Clusters | |
| } // namespace app | |
| } // namespace chip |