blob: 182ca625c11e0604002ec1506cdb86ae1136295f [file]
/*
* Copyright (c) 2026 Project CHIP 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
*
* 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.
*/
// Driver-direct unit tests covering the responsibilities the driver owns
// independently of the cluster: ReportingCondition filtering on
// OnMeasurementData, remapping of the adapter-supplied
// kSessionEndTimeReached terminal status to kPeerNotFound when no
// measurement passed the filter, and the driver-owned StartTime / EndTime
// scheduling that gates adapter->PrepareSession → StartSession → StopSession.
#include <pw_unit_test/framework.h>
#include <app/clusters/proximity-ranging-server/ProximityRangingDriver.h>
#include <app/clusters/proximity-ranging-server/RangingAdapter.h>
#include <clusters/ProximityRanging/Commands.h>
#include <clusters/ProximityRanging/Enums.h>
#include <clusters/ProximityRanging/Structs.h>
#include <lib/core/CHIPError.h>
#include <lib/support/TimerDelegateMock.h>
#include <platform/CHIPDeviceLayer.h>
#include <system/SystemClock.h>
#include <vector>
namespace {
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::ProximityRanging;
/// Records every PrepareSession/StartSession/StopSession call and exposes
/// its Callback so tests can drive OnMeasurementData /
/// OnRangingSessionStopped directly.
///
/// Under the single-shot model the driver re-invokes StartSession once per
/// rangingInstanceInterval tick. Sessions stay in "prepared" state across
/// invocations; only StopSession releases them. The mock counts every call
/// so tests can assert on cadence-driven behavior.
class MockRangingAdapter : public RangingAdapter
{
public:
explicit MockRangingAdapter(RangingTechEnum technology) : mTechnology(technology) {}
RangingTechEnum GetTechnology() const override { return mTechnology; }
Structs::RangingCapabilitiesStruct::Type GetCapabilities() const override
{
Structs::RangingCapabilitiesStruct::Type cap;
cap.technology = mTechnology;
return cap;
}
ResultCodeEnum PrepareSession(uint8_t sessionId, const StartSessionParams & params) override
{
mLastPrepareSessionId = sessionId;
mLastPrepareParams = params;
mPrepareCalls++;
if (mPrepareResult == ResultCodeEnum::kAccepted)
{
mPreparedIds.push_back(sessionId);
}
return mPrepareResult;
}
CHIP_ERROR StartSession(uint8_t sessionId) override
{
mLastStartSessionId = sessionId;
mStartCalls++;
for (auto id : mPreparedIds)
{
if (id == sessionId)
{
return mStartError;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR StopSession(uint8_t sessionId) override
{
mLastStopSessionId = sessionId;
mStopCalls++;
for (auto it = mPreparedIds.begin(); it != mPreparedIds.end(); ++it)
{
if (*it == sessionId)
{
mPreparedIds.erase(it);
if (mCallback != nullptr)
{
mCallback->OnRangingSessionStopped(sessionId, RangingSessionStatusEnum::kSessionEndTimeReached);
}
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
void StopAllSessions() override
{
while (!mPreparedIds.empty())
{
uint8_t id = mPreparedIds.back();
mPreparedIds.pop_back();
if (mCallback != nullptr)
{
mCallback->OnRangingSessionStopped(id, RangingSessionStatusEnum::kSessionEndTimeReached);
}
}
}
CHIP_ERROR GetActiveSessionIds(Span<uint8_t> & out) override
{
VerifyOrReturnError(out.size() >= mPreparedIds.size(), CHIP_ERROR_BUFFER_TOO_SMALL);
for (size_t i = 0; i < mPreparedIds.size(); i++)
{
out[i] = mPreparedIds[i];
}
out.reduce_size(mPreparedIds.size());
return CHIP_NO_ERROR;
}
std::optional<WiFiUsdConfig> GetWiFiUsdConfig() override { return mWiFiUsdConfig; }
Callback * GetCallback() const { return mCallback; }
ResultCodeEnum mPrepareResult = ResultCodeEnum::kAccepted;
/// Configurable return for StartSession. Set to CHIP_ERROR_BUSY to
/// simulate "previous measurement still in flight".
CHIP_ERROR mStartError = CHIP_NO_ERROR;
int mPrepareCalls = 0;
int mStartCalls = 0;
int mStopCalls = 0;
uint8_t mLastPrepareSessionId = 0;
uint8_t mLastStartSessionId = 0;
uint8_t mLastStopSessionId = 0;
StartSessionParams mLastPrepareParams{};
std::optional<WiFiUsdConfig> mWiFiUsdConfig;
/// Sessions for which PrepareSession returned kAccepted and StopSession
/// has not yet been invoked. Single-shot model: there is no separate
/// "active" list — measurements happen synchronously when the driver
/// calls StartSession (or via the test driving the callback directly).
std::vector<uint8_t> mPreparedIds;
private:
RangingTechEnum mTechnology;
};
/// Capture-only ProximityRangingDriver::Callback for asserting on the
/// cluster-bound stream after the driver applies its filter / status override.
class RecordingClusterCallback : public ProximityRangingDriver::Callback
{
public:
struct Measurement
{
uint8_t sessionId;
Structs::RangingMeasurementDataStruct::Type measurement;
};
struct Stopped
{
uint8_t sessionId;
RangingSessionStatusEnum status;
};
void OnMeasurementData(uint8_t sessionId, const Structs::RangingMeasurementDataStruct::Type & measurement) override
{
measurements.push_back({ sessionId, measurement });
}
void OnSessionStopped(uint8_t sessionId, RangingSessionStatusEnum status) override { stops.push_back({ sessionId, status }); }
void OnAttributeChanged(AttributeId attrId) override { attributeChanges.push_back(attrId); }
std::vector<Measurement> measurements;
std::vector<Stopped> stops;
std::vector<AttributeId> attributeChanges;
};
constexpr uint8_t kSessionId = 7;
Commands::StartRangingRequest::DecodableType MakeBleRequest()
{
Commands::StartRangingRequest::DecodableType req{};
req.technology = RangingTechEnum::kBLEBeaconRSSIRanging;
req.trigger.startTime = 0;
req.trigger.endTime = 60;
// kBLEScanningRole = active initiator. Passive-responder roles
// (kBLEBeaconRole etc.) cause the driver to suppress the kPeerNotFound
// remap, which most of these tests are not exercising.
Structs::BLERangingDeviceRoleConfigStruct::Type role;
role.role = RangingRoleEnum::kBLEScanningRole;
role.peerBLEDeviceID = 0x1234;
req.BLERangingDeviceRoleConfig.SetValue(role);
return req;
}
class TestProximityRangingDriver : public ::testing::Test
{
public:
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
};
// 1. min distance filter: measurement below min is dropped, peerFound stays false.
TEST_F(TestProximityRangingDriver, OnMeasurementDataDistanceFilterMin)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
Structs::ReportingConditionStruct::Type rc;
rc.minDistanceCondition.SetValue(50);
request.reportingCondition.SetValue(rc);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type below;
below.distance.SetNonNull(static_cast<uint16_t>(20));
ble.GetCallback()->OnMeasurementData(kSessionId, below);
EXPECT_TRUE(cb.measurements.empty());
Structs::RangingMeasurementDataStruct::Type ok;
ok.distance.SetNonNull(static_cast<uint16_t>(60));
ble.GetCallback()->OnMeasurementData(kSessionId, ok);
EXPECT_EQ(cb.measurements.size(), 1u);
driver.Shutdown();
}
// 2. max distance filter: measurement above max is dropped.
TEST_F(TestProximityRangingDriver, OnMeasurementDataDistanceFilterMax)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
Structs::ReportingConditionStruct::Type rc;
rc.maxDistanceCondition.SetValue(100);
request.reportingCondition.SetValue(rc);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type above;
above.distance.SetNonNull(static_cast<uint16_t>(200));
ble.GetCallback()->OnMeasurementData(kSessionId, above);
EXPECT_TRUE(cb.measurements.empty());
Structs::RangingMeasurementDataStruct::Type ok;
ok.distance.SetNonNull(static_cast<uint16_t>(50));
ble.GetCallback()->OnMeasurementData(kSessionId, ok);
EXPECT_EQ(cb.measurements.size(), 1u);
driver.Shutdown();
}
// 3. distance is null + a min/max condition is present → drop.
TEST_F(TestProximityRangingDriver, OnMeasurementDataDistanceNullWithCondition)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
Structs::ReportingConditionStruct::Type rc;
rc.minDistanceCondition.SetValue(50);
request.reportingCondition.SetValue(rc);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type m;
m.distance.SetNull();
ble.GetCallback()->OnMeasurementData(kSessionId, m);
EXPECT_TRUE(cb.measurements.empty());
driver.Shutdown();
}
// 4. distance is null + no distance condition → forward.
TEST_F(TestProximityRangingDriver, OnMeasurementDataDistanceNullNoCondition)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, MakeBleRequest()), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type m;
m.distance.SetNull();
ble.GetCallback()->OnMeasurementData(kSessionId, m);
EXPECT_EQ(cb.measurements.size(), 1u);
driver.Shutdown();
}
// 5. errorMargin filter: measurement with errorMargin > condition is dropped.
TEST_F(TestProximityRangingDriver, OnMeasurementDataErrorMarginFilter)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
Structs::ReportingConditionStruct::Type rc;
rc.errorMarginCondition.SetValue(10);
request.reportingCondition.SetValue(rc);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type sloppy;
sloppy.distance.SetNonNull(static_cast<uint16_t>(100));
sloppy.errorMargin.SetValue(static_cast<uint16_t>(50));
ble.GetCallback()->OnMeasurementData(kSessionId, sloppy);
EXPECT_TRUE(cb.measurements.empty());
Structs::RangingMeasurementDataStruct::Type tight;
tight.distance.SetNonNull(static_cast<uint16_t>(100));
tight.errorMargin.SetValue(static_cast<uint16_t>(5));
ble.GetCallback()->OnMeasurementData(kSessionId, tight);
EXPECT_EQ(cb.measurements.size(), 1u);
driver.Shutdown();
}
// 6. No reportingCondition → all measurements forwarded.
TEST_F(TestProximityRangingDriver, OnMeasurementDataNoReportingCondition)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, MakeBleRequest()), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type m;
m.distance.SetNonNull(static_cast<uint16_t>(123));
m.errorMargin.SetValue(static_cast<uint16_t>(99));
ble.GetCallback()->OnMeasurementData(kSessionId, m);
EXPECT_EQ(cb.measurements.size(), 1u);
driver.Shutdown();
}
// 7. Adapter terminates with kSessionEndTimeReached after no measurement
// passed the filter → driver remaps to kPeerNotFound.
TEST_F(TestProximityRangingDriver, TerminationStatusPeerNotFoundWhenNoMeasurementPassedFilter)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
Structs::ReportingConditionStruct::Type rc;
rc.minDistanceCondition.SetValue(1000);
request.reportingCondition.SetValue(rc);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type tooClose;
tooClose.distance.SetNonNull(static_cast<uint16_t>(50));
ble.GetCallback()->OnMeasurementData(kSessionId, tooClose);
EXPECT_TRUE(cb.measurements.empty());
ble.GetCallback()->OnRangingSessionStopped(kSessionId, RangingSessionStatusEnum::kSessionEndTimeReached);
ASSERT_EQ(cb.stops.size(), 1u);
EXPECT_EQ(cb.stops[0].status, RangingSessionStatusEnum::kPeerNotFound);
driver.Shutdown();
}
// 8. At least one passing measurement → cluster sees kSessionEndTimeReached.
TEST_F(TestProximityRangingDriver, TerminationStatusEndTimeReached)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, MakeBleRequest()), ResultCodeEnum::kAccepted);
Structs::RangingMeasurementDataStruct::Type m;
m.distance.SetNonNull(static_cast<uint16_t>(100));
ble.GetCallback()->OnMeasurementData(kSessionId, m);
EXPECT_EQ(cb.measurements.size(), 1u);
ble.GetCallback()->OnRangingSessionStopped(kSessionId, RangingSessionStatusEnum::kSessionEndTimeReached);
ASSERT_EQ(cb.stops.size(), 1u);
EXPECT_EQ(cb.stops[0].status, RangingSessionStatusEnum::kSessionEndTimeReached);
driver.Shutdown();
}
// 9. Adapter-driven kHardwareError passes through verbatim regardless of
// peerFound state.
TEST_F(TestProximityRangingDriver, AdapterDrivenHardwareErrorPassthrough)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, MakeBleRequest()), ResultCodeEnum::kAccepted);
ble.GetCallback()->OnRangingSessionStopped(kSessionId, RangingSessionStatusEnum::kHardwareError);
ASSERT_EQ(cb.stops.size(), 1u);
EXPECT_EQ(cb.stops[0].status, RangingSessionStatusEnum::kHardwareError);
driver.Shutdown();
}
// 10. StartSessionParams forwards role / band fields verbatim and intentionally
// does NOT carry rangingInstanceInterval, startTime, or endTime — those
// are entirely driver-owned. The peerBLEDeviceID set by MakeBleRequest
// proves role configs are forwarded.
TEST_F(TestProximityRangingDriver, StartSessionParamsForwardsRoleConfigsNotTriggerOrInterval)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
request.trigger.endTime = 25;
request.trigger.rangingInstanceInterval.SetValue(3);
Structs::ReportingConditionStruct::Type rc;
rc.minDistanceCondition.SetValue(10);
request.reportingCondition.SetValue(rc);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
// Adapter-facing params carry the role config (peerBLEDeviceID from
// MakeBleRequest is 0x1234) but no trigger / interval fields exist on
// StartSessionParams at all.
ASSERT_TRUE(ble.mLastPrepareParams.bleRoleConfig.has_value());
Structs::BLERangingDeviceRoleConfigStruct::DecodableType empty{};
EXPECT_EQ(ble.mLastPrepareParams.bleRoleConfig.value_or(empty).peerBLEDeviceID, 0x1234u);
driver.Shutdown();
}
// 11. startTime == 0 → driver invokes PrepareSession then StartSession
// synchronously, no timer fire required.
TEST_F(TestProximityRangingDriver, ZeroStartTimeFiresPrepareThenStartSynchronously)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
EXPECT_EQ(ble.mPrepareCalls, 1);
EXPECT_EQ(ble.mStartCalls, 1);
EXPECT_EQ(ble.mLastPrepareSessionId, kSessionId);
EXPECT_EQ(ble.mLastStartSessionId, kSessionId);
driver.Shutdown();
}
// 12. startTime > 0 → driver invokes PrepareSession immediately but defers
// StartSession until the start timer fires. SessionIDList is dirty
// immediately because the sessionId is acceptance-visible.
TEST_F(TestProximityRangingDriver, NonZeroStartTimeDefersStartUntilTimerFires)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 5;
request.trigger.endTime = 60;
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
EXPECT_EQ(ble.mPrepareCalls, 1);
EXPECT_EQ(ble.mStartCalls, 0);
EXPECT_EQ(driver.GetNumActiveSessionIds(), 1u);
EXPECT_FALSE(cb.attributeChanges.empty());
// Advance just shy of the start deadline; StartSession must still not
// have been invoked.
timer.AdvanceClock(System::Clock::Milliseconds32(4'000));
EXPECT_EQ(ble.mStartCalls, 0);
// Cross the 5s threshold; the driver must invoke StartSession exactly once.
timer.AdvanceClock(System::Clock::Milliseconds32(2'000));
EXPECT_EQ(ble.mStartCalls, 1);
driver.Shutdown();
}
// 13. End timer fires while session is active → driver invokes StopSession.
// Uses startTime == 0 so only the end timer is armed (TimerDelegateMock
// tracks only one timer at a time, so testing both start and end timers
// simultaneously requires a richer mock; the more interesting scenario
// for the driver is the end-time cutoff itself, exercised here).
TEST_F(TestProximityRangingDriver, EndTimeFiresWhileSessionActive)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
request.trigger.endTime = 10;
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
EXPECT_EQ(ble.mStartCalls, 1);
timer.AdvanceClock(System::Clock::Milliseconds32(11'000));
EXPECT_EQ(ble.mStopCalls, 1);
EXPECT_EQ(ble.mLastStopSessionId, kSessionId);
EXPECT_EQ(driver.GetNumActiveSessionIds(), 0u);
driver.Shutdown();
}
// 14. Adapter rejects in PrepareSession → no session record committed,
// SessionIDList is not made dirty, no timers armed.
TEST_F(TestProximityRangingDriver, PrepareSessionRejectedByAdapter)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
ble.mPrepareResult = ResultCodeEnum::kRejectedInfeasibleRanging;
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
EXPECT_EQ(driver.HandleStartRanging(kSessionId, MakeBleRequest()), ResultCodeEnum::kRejectedInfeasibleRanging);
EXPECT_EQ(ble.mPrepareCalls, 1);
EXPECT_EQ(ble.mStartCalls, 0);
EXPECT_EQ(ble.mStopCalls, 0);
EXPECT_EQ(driver.GetNumActiveSessionIds(), 0u);
EXPECT_TRUE(cb.attributeChanges.empty());
driver.Shutdown();
}
// 15. Shutdown cancels per-session timers so they cannot fire post-shutdown.
TEST_F(TestProximityRangingDriver, ShutdownCancelsPendingTimers)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 5;
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
driver.Shutdown();
// After Shutdown, advancing past the start deadline must not produce a
// late StartSession invocation.
const int startCallsAtShutdown = ble.mStartCalls;
timer.AdvanceClock(System::Clock::Milliseconds32(60'000));
EXPECT_EQ(ble.mStartCalls, startCallsAtShutdown);
}
// 16. Periodic ranging: driver re-invokes StartSession on every interval tick
// (anchored from the moment StartSession was issued, not from when the
// measurement returns). With startTime==0 and interval=2s, advancing the
// clock 5 seconds should produce the initial StartSession plus two
// additional ticks (at t=2s and t=4s).
TEST_F(TestProximityRangingDriver, PeriodicRangingDriverReinvokesStartSession)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
request.trigger.endTime = 60;
request.trigger.rangingInstanceInterval.SetValue(2);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
// Initial synchronous StartSession invocation.
EXPECT_EQ(ble.mStartCalls, 1);
// First periodic tick at t=2s.
timer.AdvanceClock(System::Clock::Milliseconds32(2'000));
EXPECT_EQ(ble.mStartCalls, 2);
// Second periodic tick at t=4s.
timer.AdvanceClock(System::Clock::Milliseconds32(2'000));
EXPECT_EQ(ble.mStartCalls, 3);
driver.Shutdown();
}
// 17. Instant ranging: the FIRST measurement that satisfies ReportingCondition
// causes the driver to call adapter->StopSession synchronously, terminating
// the session before EndTime fires. The cluster sees a single
// RangingResult event followed by RangingSessionStatus(SessionEndTimeReached).
TEST_F(TestProximityRangingDriver, InstantRangingTerminatesOnFirstPassingMeasurement)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
request.trigger.endTime = 60;
// No rangingInstanceInterval -> instant ranging.
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
EXPECT_EQ(ble.mStartCalls, 1);
// Adapter delivers a passing measurement. Driver forwards it AND calls
// StopSession (instant ranging termination).
Structs::RangingMeasurementDataStruct::Type m;
m.distance.SetNonNull(static_cast<uint16_t>(100));
ble.GetCallback()->OnMeasurementData(kSessionId, m);
EXPECT_EQ(cb.measurements.size(), 1u);
EXPECT_EQ(ble.mStopCalls, 1);
EXPECT_EQ(driver.GetNumActiveSessionIds(), 0u);
ASSERT_EQ(cb.stops.size(), 1u);
EXPECT_EQ(cb.stops[0].status, RangingSessionStatusEnum::kSessionEndTimeReached);
driver.Shutdown();
}
// 18. Instant ranging + filtered measurement: the driver drops the
// measurement and does NOT re-issue StartSession. Per spec, instant
// ranging is a single ranging attempt; the session lingers until
// EndTime fires, at which point the kPeerNotFound remap produces the
// spec-correct outcome.
TEST_F(TestProximityRangingDriver, InstantRangingFilteredMeasurementDoesNotRetry)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
request.trigger.endTime = 60;
Structs::ReportingConditionStruct::Type rc;
rc.minDistanceCondition.SetValue(1000); // 100cm measurement will be filtered
request.reportingCondition.SetValue(rc);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
EXPECT_EQ(ble.mStartCalls, 1);
Structs::RangingMeasurementDataStruct::Type tooClose;
tooClose.distance.SetNonNull(static_cast<uint16_t>(100));
ble.GetCallback()->OnMeasurementData(kSessionId, tooClose);
// Measurement was filtered: not forwarded; driver did NOT re-issue
// StartSession; session stays alive until EndTime.
EXPECT_TRUE(cb.measurements.empty());
EXPECT_EQ(ble.mStopCalls, 0);
EXPECT_EQ(ble.mStartCalls, 1);
EXPECT_EQ(driver.GetNumActiveSessionIds(), 1u);
driver.Shutdown();
}
// 19a. Passive-responder sessions (BLE beacon, Wi-Fi publisher, BLT-CS
// reflector) do NOT trigger the kPeerNotFound remap when the session
// ends without any measurement. The absence of measurements is the
// expected outcome for a responder, not a peer-not-found condition.
TEST_F(TestProximityRangingDriver, PassiveResponderSessionDoesNotRemapToPeerNotFound)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
// Start a passive-responder session (kBLEBeaconRole).
Commands::StartRangingRequest::DecodableType request{};
request.technology = RangingTechEnum::kBLEBeaconRSSIRanging;
request.trigger.startTime = 0;
request.trigger.endTime = 60;
Structs::BLERangingDeviceRoleConfigStruct::Type role;
role.role = RangingRoleEnum::kBLEBeaconRole; // passive responder
role.peerBLEDeviceID = 0x1234;
request.BLERangingDeviceRoleConfig.SetValue(role);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
// No measurement is delivered; adapter terminates with kSessionEndTimeReached
// (e.g. driver-issued StopSession at end-time). Driver MUST NOT remap to
// kPeerNotFound for the responder side.
ble.GetCallback()->OnRangingSessionStopped(kSessionId, RangingSessionStatusEnum::kSessionEndTimeReached);
ASSERT_EQ(cb.stops.size(), 1u);
EXPECT_EQ(cb.stops[0].status, RangingSessionStatusEnum::kSessionEndTimeReached);
driver.Shutdown();
}
// 19. Periodic ranging + adapter returns CHIP_ERROR_BUSY on a tick: the driver
// must continue scheduling subsequent ticks; the busy tick is just skipped.
TEST_F(TestProximityRangingDriver, PeriodicRangingBusyAdapterDoesNotRetireSession)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
ble.mStartError = CHIP_ERROR_BUSY;
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
request.trigger.endTime = 60;
request.trigger.rangingInstanceInterval.SetValue(2);
ASSERT_EQ(driver.HandleStartRanging(kSessionId, request), ResultCodeEnum::kAccepted);
// Even though every StartSession returns CHIP_ERROR_BUSY, the session
// remains alive and the periodic schedule keeps firing.
EXPECT_EQ(ble.mStartCalls, 1);
timer.AdvanceClock(System::Clock::Milliseconds32(2'000));
EXPECT_EQ(ble.mStartCalls, 2);
timer.AdvanceClock(System::Clock::Milliseconds32(2'000));
EXPECT_EQ(ble.mStartCalls, 3);
EXPECT_EQ(driver.GetNumActiveSessionIds(), 1u);
EXPECT_EQ(ble.mStopCalls, 0);
driver.Shutdown();
}
// 21. Init can only be called once between Shutdown calls. A second Init
// without an intervening Shutdown surfaces CHIP_ERROR_INCORRECT_STATE so
// a second cluster cannot silently steal the callback wiring.
TEST_F(TestProximityRangingDriver, InitTwiceFailsWithoutShutdown)
{
TimerDelegateMock timer;
ProximityRangingDriver driver{ {}, timer };
struct Sink : public ProximityRangingDriver::Callback
{
void OnMeasurementData(uint8_t, const Structs::RangingMeasurementDataStruct::Type &) override {}
void OnSessionStopped(uint8_t, RangingSessionStatusEnum) override {}
void OnAttributeChanged(AttributeId) override {}
} sink;
EXPECT_EQ(driver.Init(sink), CHIP_NO_ERROR);
EXPECT_EQ(driver.Init(sink), CHIP_ERROR_INCORRECT_STATE);
driver.Shutdown();
// After Shutdown, Init must succeed again.
EXPECT_EQ(driver.Init(sink), CHIP_NO_ERROR);
driver.Shutdown();
}
// 22. Shutdown drives StopSession on every active session and releases the
// pool slots so a re-Init starts clean.
TEST_F(TestProximityRangingDriver, ShutdownStopsActiveSessions)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
ASSERT_EQ(driver.HandleStartRanging(11, request), ResultCodeEnum::kAccepted);
ASSERT_EQ(driver.HandleStartRanging(12, request), ResultCodeEnum::kAccepted);
ASSERT_EQ(driver.GetNumActiveSessionIds(), 2u);
// Shutdown must drive each adapter's StopSession path; the mock then
// routes OnRangingSessionStopped back, which RetireSession releases.
driver.Shutdown();
EXPECT_EQ(driver.GetNumActiveSessionIds(), 0u);
}
// 23. GetActiveSessionIds returns CHIP_ERROR_BUFFER_TOO_SMALL when the
// caller-supplied span cannot hold every active session ID.
TEST_F(TestProximityRangingDriver, GetActiveSessionIdsBufferTooSmall)
{
TimerDelegateMock timer;
MockRangingAdapter ble(RangingTechEnum::kBLEBeaconRSSIRanging);
RangingAdapter * adapters[] = { &ble };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
RecordingClusterCallback cb;
ASSERT_EQ(driver.Init(cb), CHIP_NO_ERROR);
auto request = MakeBleRequest();
request.trigger.startTime = 0;
ASSERT_EQ(driver.HandleStartRanging(1, request), ResultCodeEnum::kAccepted);
ASSERT_EQ(driver.HandleStartRanging(2, request), ResultCodeEnum::kAccepted);
uint8_t buffer[1] = {};
Span<uint8_t> tooSmall(buffer, 1);
EXPECT_EQ(driver.GetActiveSessionIds(tooSmall), CHIP_ERROR_BUFFER_TOO_SMALL);
driver.Shutdown();
}
// 24. GetWiFiUsdConfig falls back to the kWiFiNextGenerationRanging adapter
// when no kWiFiRoundTripTimeRanging adapter is bound. Exercises the
// second-branch lookup.
TEST_F(TestProximityRangingDriver, GetWiFiUsdConfigViaNextGenerationAdapter)
{
TimerDelegateMock timer;
MockRangingAdapter ngAdapter(RangingTechEnum::kWiFiNextGenerationRanging);
WiFiUsdConfig cfg{};
memset(cfg.deviceIdentityKey, 0x5A, sizeof(cfg.deviceIdentityKey));
ngAdapter.mWiFiUsdConfig = cfg;
RangingAdapter * adapters[] = { &ngAdapter };
ProximityRangingDriver driver{ Span<RangingAdapter * const>(adapters), timer };
auto resolved = driver.GetWiFiUsdConfig();
ASSERT_TRUE(resolved.has_value());
WiFiUsdConfig empty{};
EXPECT_EQ(resolved.value_or(empty).deviceIdentityKey[0], 0x5A);
}
} // namespace