[seeker] optimize candidate storage and connection strategy (#12389)

This commit updates `Seeker` to use `MessageBackedArray` for storing
discovered candidates. This moves candidate storage from statically
allocated RAM in the `Seeker` instance to a `Message` buffer,
reducing the resident memory footprint.

The connection logic is enhanced to prioritize network diversity. The
`Seeker` now attempts to connect to the most favored candidate of
each unique network (Extended PAN ID) first. After exhausting unique
networks, it proceeds to remaining backup candidates. This improves
the likelihood of finding the correct network quickly in multi-network
environments.

The candidate storage policy is updated to limit entries per network
(defined by `kMaxCandidatesPerNetwork`). When storage is full, the
`Seeker` evicts a redundant entry from a well-represented network to
accommodate new network discoveries.

A detailed unit test `test_seeker` is included to validate candidate
tracking and selection order under various scenarios.
This commit is contained in:
Abtin Keshavarzian
2026-02-12 18:46:59 -06:00
committed by GitHub
parent 81ad265139
commit 16cb5ff87d
6 changed files with 889 additions and 90 deletions
+11 -2
View File
@@ -55,10 +55,19 @@
/**
* @def OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES
*
* The maximum number of Joiner Router entries that can be queued by the Joiner.
* The maximum number of Joiner Router candidates that can be tracked by the Joiner/Seeker.
*/
#ifndef OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES
#define OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES 2
#define OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES 16
#endif
/**
* @def OPENTHREAD_CONFIG_JOINER_CANDIDATES_PER_NETWORK
*
* The maximum number of Joiner Router candidates to track per network (Extended PAN ID) by the Joiner/Seeker.
*/
#ifndef OPENTHREAD_CONFIG_JOINER_CANDIDATES_PER_NETWORK
#define OPENTHREAD_CONFIG_JOINER_CANDIDATES_PER_NETWORK 3
#endif
/**
+307 -70
View File
@@ -46,7 +46,7 @@ Seeker::Seeker(Instance &aInstance)
: InstanceLocator(aInstance)
, mState(kStateStopped)
, mUdpPort(kDefaultUdpPort)
, mCandidateIndex(0)
, mCandidates(aInstance)
{
}
@@ -66,9 +66,6 @@ Error Seeker::Start(ScanEvaluator aScanEvaluator, void *aContext)
mScanEvaluator.Set(aScanEvaluator, aContext);
ClearAllBytes(mCandidates);
mCandidateIndex = 0;
SuccessOrExit(error =
Get<Mle::DiscoverScanner>().Discover(Mac::ChannelMask(0), Get<Mac::Mac>().GetPanId(),
/* aJoiner */ true, /* aEnableFiltering */ false,
@@ -87,11 +84,14 @@ void Seeker::Stop(void)
case kStateDiscovering:
case kStateDiscoverDone:
break;
case kStateConnecting:
case kStateConnectingNetworks:
case kStateConnectingAny:
IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(mUdpPort));
break;
}
mCandidates.Clear();
SetState(kStateStopped);
}
@@ -152,75 +152,109 @@ exit:
void Seeker::SaveCandidate(const ScanResult &aResult, bool aPreferred)
{
uint8_t priority;
Candidate *end;
Candidate *entry;
Error error = kErrorNone;
const MeshCoP::ExtendedPanId &extPanId = AsCoreType(&aResult.mExtendedPanId);
bool shouldPushAsNew = false;
CandidateEntry entry;
LogInfo("Discovered: %s, pan:0x%04x, port:%u, chan:%u, rssi:%d, preferred:%s",
AsCoreType(&aResult.mExtAddress).ToString().AsCString(), aResult.mPanId, aResult.mJoinerUdpPort,
aResult.mChannel, aResult.mRssi, ToYesNo(aPreferred));
entry.MarkAsEmpty();
priority = CalculatePriority(aResult.mRssi, aPreferred);
// We keep the list sorted based on priority. Find the place to
// add the new result.
end = GetArrayEnd(mCandidates);
for (entry = &mCandidates[0]; entry < end; entry++)
if (mCandidates.FindMatching(entry, extPanId, AsCoreType(&aResult.mExtAddress)) == kErrorNone)
{
if (priority > entry->mPriority)
entry.Log(Candidate::kReplace);
}
else
{
uint16_t count = CountAndSelectLeastFavoredCandidateFor(extPanId, entry);
if ((count == kMaxCandidatesPerNetwork) || ((count > 0) && mCandidates.IsFull()))
{
// We have reached the max allowed candidates for this
// network (`extPanId`), or the array is full but we have
// some existing entries for this network. In both cases
// we replace the least favored existing entry with the
// new scan result only if the new result is favored over
// the existing one.
if (entry.IsFavoredOver(aResult, aPreferred))
{
error = kErrorDrop;
}
else
{
entry.Log(Candidate::kReplace);
}
}
else if (!mCandidates.IsFull())
{
shouldPushAsNew = true;
}
else
{
// When the array is full, we only evict an entry to make
// room for a new network (one not yet in the list). We
// do not evict to add redundancy (additional candidates)
// for a network we have already discovered. We know
// `count == 0` when we get here.
error = EvictCandidate(entry);
}
}
entry.SetFrom(aResult, aPreferred);
// We check the `error` after `entry.SetFrom()` so that the
// `entry.Log()` call at `exit` uses the proper candidate
// entry from the scan `aResult`.
SuccessOrExit(error);
SuccessOrExit(error = shouldPushAsNew ? mCandidates.Push(entry) : mCandidates.Write(entry));
entry.Log(Candidate::kSave);
exit:
if (error != kErrorNone)
{
entry.Log(Candidate::kDrop);
}
}
Error Seeker::EvictCandidate(CandidateEntry &aEntry)
{
Error error = kErrorNoBufs;
CandidateEntry entry;
aEntry.MarkAsEmpty();
for (entry.InitForIteration(); mCandidates.ReadNext(entry) == kErrorNone;)
{
uint16_t count = CountAndSelectLeastFavoredCandidateFor(entry.mExtPanId, aEntry);
if (count > 1)
{
aEntry.Log(Candidate::kEvict);
error = kErrorNone;
break;
}
}
VerifyOrExit(entry < end);
// Shift elements in array to make room for the new one.
memmove(entry + 1, entry,
static_cast<size_t>(reinterpret_cast<uint8_t *>(end - 1) - reinterpret_cast<uint8_t *>(entry)));
entry->mExtAddr = AsCoreType(&aResult.mExtAddress);
entry->mPanId = aResult.mPanId;
entry->mJoinerUdpPort = aResult.mJoinerUdpPort;
entry->mChannel = aResult.mChannel;
entry->mPriority = priority;
exit:
return;
}
uint8_t Seeker::CalculatePriority(int8_t aRssi, bool aPreferred)
{
int16_t priority;
if (aRssi == Radio::kInvalidRssi)
{
aRssi = -127;
}
priority = Clamp<int8_t>(aRssi, -127, -1);
// We assign a higher priority value to networks marked as
// preferred (128 < priority < 256) compared to normal
// (0 < priority < 128). Sub-prioritize based on signal
// strength. Priority 0 is reserved for unused entry.
priority += aPreferred ? 256 : 128;
return static_cast<uint8_t>(priority);
return error;
}
Error Seeker::SetUpNextConnection(Ip6::SockAddr &aSockAddr)
{
Error error = kErrorNone;
const Candidate *candidate;
Error error = kErrorNone;
CandidateEntry entry;
switch (GetState())
{
case kStateDiscoverDone:
case kStateConnecting:
SetState(kStateConnectingNetworks);
break;
case kStateConnectingNetworks:
case kStateConnectingAny:
break;
case kStateStopped:
@@ -228,37 +262,240 @@ Error Seeker::SetUpNextConnection(Ip6::SockAddr &aSockAddr)
ExitNow(error = kErrorInvalidState);
}
candidate = &mCandidates[mCandidateIndex];
error = SelectNextCandidate(entry);
if (!candidate->IsValid())
if (error != kErrorNone)
{
Stop();
ExitNow(error = kErrorNotFound);
ExitNow();
}
mCandidateIndex++;
entry.Log(Candidate::kConnect);
LogInfo("Setting up conn to %s, pan:0x%04x, chan:%u", candidate->mExtAddr.ToString().AsCString(), candidate->mPanId,
candidate->mChannel);
entry.mConnAttempted = true;
IgnoreError(mCandidates.Write(entry));
Get<Mac::Mac>().SetPanId(candidate->mPanId);
SuccessOrExit(error = Get<Mac::Mac>().SetPanChannel(candidate->mChannel));
Get<Mac::Mac>().SetPanId(entry.mPanId);
SuccessOrExit(error = Get<Mac::Mac>().SetPanChannel(entry.mChannel));
if (!Get<Ip6::Filter>().IsUnsecurePort(mUdpPort))
{
SuccessOrExit(error = Get<Ip6::Filter>().AddUnsecurePort(mUdpPort));
}
SetState(kStateConnecting);
aSockAddr.Clear();
aSockAddr.SetPort(candidate->mJoinerUdpPort);
aSockAddr.GetAddress().SetToLinkLocalAddress(candidate->mExtAddr);
aSockAddr.SetPort(entry.mJoinerUdpPort);
aSockAddr.GetAddress().SetToLinkLocalAddress(entry.mExtAddr);
exit:
return error;
}
Error Seeker::SelectNextCandidate(CandidateEntry &aEntry)
{
CandidateEntry entry;
aEntry.MarkAsEmpty();
if (mState == kStateConnectingNetworks)
{
// While in `kStateConnectingNetworks` we first try to cover all
// discovered networks (Extended PAN IDs). We determine the most
// favored candidate among all discovered networks which has not
// yet been attempted.
for (entry.InitForIteration(); mCandidates.ReadNext(entry) == kErrorNone;)
{
CandidateEntry matchingPanEntry;
if (SelectMostFavoredCandidateFor(entry.mExtPanId, matchingPanEntry) == kErrorNone)
{
aEntry.ReplaceWithIfFavored(matchingPanEntry);
}
}
if (!aEntry.IsEmpty())
{
ExitNow();
}
// If we have already covered the most favored candidate per Network
// (Extended PAN ID), we switch to `kStateConnectingAny` where we
// try any remaining discovered candidates (e.g. backup candidates
// associated with the same networks).
mState = kStateConnectingAny;
}
for (entry.InitForIteration(); mCandidates.ReadNext(entry) == kErrorNone;)
{
if (entry.mConnAttempted)
{
continue;
}
aEntry.ReplaceWithIfFavored(entry);
}
exit:
return aEntry.IsEmpty() ? kErrorNotFound : kErrorNone;
}
uint16_t Seeker::CountAndSelectLeastFavoredCandidateFor(const MeshCoP::ExtendedPanId &aExtPanId,
CandidateEntry &aEntry) const
{
// Iterates through all candidates matching a given Extended PAN ID.
// Returns the total count of such entries. Also finds the least
// favored matching entry and returns it in `aEntry`. This is then
// used to decide whether to replace an existing candidate entry with
// a new one.
uint16_t count = 0;
CandidateEntry entry;
aEntry.MarkAsEmpty();
for (entry.InitForIteration(); mCandidates.ReadNext(entry) == kErrorNone;)
{
if (!entry.Matches(aExtPanId))
{
continue;
}
count++;
if ((count == 1) || aEntry.IsFavoredOver(entry))
{
aEntry = entry;
}
}
return count;
}
Error Seeker::SelectMostFavoredCandidateFor(const MeshCoP::ExtendedPanId &aExtPanId,
CandidateEntry &aFavoredEntry) const
{
// Iterates through all candidates associated with a given network
// (matching `aExtPanId`). If a connection has already been attempted
// with any candidate from this network, `kErrorAlready` is returned.
// Otherwise, the most favored candidate for this network is determined
// and returned in `aFavoredEntry`.
Error error = kErrorNone;
CandidateEntry entry;
aFavoredEntry.MarkAsEmpty();
for (entry.InitForIteration(); mCandidates.ReadNext(entry) == kErrorNone;)
{
if (!entry.Matches(aExtPanId))
{
continue;
}
if (entry.mConnAttempted)
{
error = kErrorAlready;
ExitNow();
}
aFavoredEntry.ReplaceWithIfFavored(entry);
}
exit:
return error;
}
//---------------------------------------------------------------------------------------------------------------------
// Seeker::Candidate
void Seeker::Candidate::SetFrom(const ScanResult &aResult, bool aPreferred)
{
mExtPanId = AsCoreType(&aResult.mExtendedPanId);
mExtAddr = AsCoreType(&aResult.mExtAddress);
mPanId = aResult.mPanId;
mJoinerUdpPort = aResult.mJoinerUdpPort;
mChannel = aResult.mChannel;
mRssi = aResult.mRssi;
mPreferred = aPreferred;
mConnAttempted = false;
}
bool Seeker::Candidate::IsFavoredOver(const Candidate &aOther) const
{
return IsFavoredOver(aOther.mRssi, aOther.mPreferred);
}
bool Seeker::Candidate::IsFavoredOver(const ScanResult &aResult, bool aPreferred) const
{
return IsFavoredOver(aResult.mRssi, aPreferred);
}
bool Seeker::Candidate::IsFavoredOver(int8_t aRssi, bool aPreferred) const
{
int compare;
compare = ThreeWayCompare(mPreferred, aPreferred);
VerifyOrExit(compare == 0);
compare = ThreeWayCompare(mRssi, aRssi);
exit:
return (compare > 0);
}
bool Seeker::Candidate::Matches(const MeshCoP::ExtendedPanId &aExtPanId, const Mac::ExtAddress &aExtAddr) const
{
return (mExtPanId == aExtPanId) && (mExtAddr == aExtAddr);
}
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
const char *Seeker::Candidate::ActionToString(Action aAction)
{
#define ActionMapList(_) \
_(kSave, "Saving") \
_(kReplace, "Replacing") \
_(kEvict, "Evicting") \
_(kDrop, "Dropping") \
_(kConnect, "Connecting to")
DefineEnumStringArray(ActionMapList);
return kStrings[aAction];
}
void Seeker::Candidate::Log(Action aAction) const
{
LogInfo("%s candidate:", ActionToString(aAction));
LogInfo(" ext-panid: %s", mExtPanId.ToString().AsCString());
LogInfo(" ext-addr: %s", mExtAddr.ToString().AsCString());
LogInfo(" panid: 0x%04x", mPanId);
LogInfo(" channel: %u", mChannel);
LogInfo(" rssi: %d", mRssi);
LogInfo(" preferred: %s", ToYesNo(mPreferred));
LogInfo(" joiner-port: %u", mJoinerUdpPort);
}
#else
void Seeker::Candidate::Log(Action) const {}
#endif
//----------------------------------------------------------------------------------------------------------------------
// Seeker::CandidateEntry
void Seeker::CandidateEntry::ReplaceWithIfFavored(const CandidateEntry &aEntry)
{
// Replaces this entry with `aEntry` if this entry is currently
// empty (not yet set) or if `aEntry` is favored over the
// current one.
if (IsEmpty() || aEntry.IsFavoredOver(*this))
{
*this = aEntry;
}
}
} // namespace MeshCoP
} // namespace ot
+62 -18
View File
@@ -48,12 +48,18 @@
#include "common/callback.hpp"
#include "common/error.hpp"
#include "common/locator.hpp"
#include "common/msg_backed_array.hpp"
#include "common/non_copyable.hpp"
#include "common/numeric_limits.hpp"
#include "mac/mac_types.hpp"
#include "meshcop/extended_panid.hpp"
#include "net/socket.hpp"
#include "thread/discover_scanner.hpp"
namespace ot {
class UnitTester;
namespace MeshCoP {
/**
@@ -61,6 +67,8 @@ namespace MeshCoP {
*/
class Seeker : public InstanceLocator, private NonCopyable
{
friend class ot::UnitTester;
public:
typedef otSeekerScanResult ScanResult; ///< Discover Scan result.
@@ -166,40 +174,76 @@ public:
Error SetUpNextConnection(Ip6::SockAddr &aSockAddr);
private:
static constexpr uint16_t kDefaultUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT;
static constexpr uint16_t kMaxCandidates = OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES;
static constexpr uint16_t kDefaultUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT;
static constexpr uint16_t kMaxCandidates = OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES;
static constexpr uint16_t kMaxCandidatesPerNetwork = OPENTHREAD_CONFIG_JOINER_CANDIDATES_PER_NETWORK;
enum State : uint8_t
{
kStateStopped,
kStateDiscovering,
kStateDiscoverDone,
kStateConnecting,
kStateConnectingNetworks,
kStateConnectingAny,
};
struct Candidate
struct Candidate : public Clearable<Candidate>
{
bool IsValid(void) const { return mPriority != 0; }
enum Action : uint8_t
{
kSave,
kReplace,
kEvict,
kDrop,
kConnect,
};
Mac::ExtAddress mExtAddr;
Mac::PanId mPanId;
uint16_t mJoinerUdpPort;
uint8_t mChannel;
uint8_t mPriority;
Candidate(void) { Clear(); }
void SetFrom(const ScanResult &aResult, bool aPreferred);
bool IsFavoredOver(const Candidate &aOther) const;
bool IsFavoredOver(const ScanResult &aResult, bool aPreferred) const;
bool IsFavoredOver(int8_t aRssi, bool aPreferred) const;
bool Matches(const MeshCoP::ExtendedPanId &aExtPanId) const { return mExtPanId == aExtPanId; }
bool Matches(const MeshCoP::ExtendedPanId &aExtPanId, const Mac::ExtAddress &aExtAddr) const;
void Log(Action aAction) const;
static const char *ActionToString(Action aAction);
MeshCoP::ExtendedPanId mExtPanId;
Mac::ExtAddress mExtAddr;
Mac::PanId mPanId;
uint16_t mJoinerUdpPort;
uint8_t mChannel;
int8_t mRssi;
bool mPreferred : 1;
bool mConnAttempted : 1;
};
State GetState(void) const { return mState; }
void SetState(State aState) { mState = aState; }
static void HandleDiscoverResult(ScanResult *aResult, void *aContext);
void HandleDiscoverResult(ScanResult *aResult);
void SaveCandidate(const ScanResult &aResult, bool aPreferred);
static uint8_t CalculatePriority(int8_t aRssi, bool aPreferred);
using CandidateArray = MessageBackedArray<Candidate, kMaxCandidates>;
struct CandidateEntry : public CandidateArray::IndexedEntry
{
CandidateEntry(void) { MarkAsEmpty(); }
void MarkAsEmpty(void) { SetIndexToInvalid(); }
bool IsEmpty(void) const { return IsIndexInvalid(); }
void ReplaceWithIfFavored(const CandidateEntry &aEntry);
};
State GetState(void) const { return mState; }
void SetState(State aState) { mState = aState; }
static void HandleDiscoverResult(ScanResult *aResult, void *aContext);
void HandleDiscoverResult(ScanResult *aResult);
void SaveCandidate(const ScanResult &aResult, bool aPreferred);
Error EvictCandidate(CandidateEntry &aEntry);
Error SelectNextCandidate(CandidateEntry &aEntry);
uint16_t CountAndSelectLeastFavoredCandidateFor(const MeshCoP::ExtendedPanId &aExtPanId,
CandidateEntry &aEntry) const;
Error SelectMostFavoredCandidateFor(const MeshCoP::ExtendedPanId &aExtPanId, CandidateEntry &aFavoredEntry) const;
State mState;
uint16_t mUdpPort;
Callback<ScanEvaluator> mScanEvaluator;
Candidate mCandidates[kMaxCandidates];
uint16_t mCandidateIndex;
CandidateArray mCandidates;
};
} // namespace MeshCoP
@@ -55,6 +55,10 @@
#define OPENTHREAD_CONFIG_DNS_CLIENT_BIND_UDP_TO_THREAD_NETIF 1
#define OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES 8
#define OPENTHREAD_CONFIG_JOINER_CANDIDATES_PER_NETWORK 3
#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 1
#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION 1
+1
View File
@@ -251,6 +251,7 @@ ot_unit_test(power_calibration)
ot_unit_test(priority_queue)
ot_unit_test(pskc)
ot_unit_test(routing_manager)
ot_unit_test(seeker)
ot_unit_test(serial_number)
ot_unit_test(smart_ptrs)
ot_unit_test(spinel_buffer)
+504
View File
@@ -0,0 +1,504 @@
/*
* Copyright (c) 2026, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "instance/instance.hpp"
#include "test_platform.h"
#include "test_util.hpp"
namespace ot {
#if OPENTHREAD_CONFIG_SEEKER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE
class UnitTester
{
public:
using Seeker = MeshCoP::Seeker;
using ScanResult = MeshCoP::Seeker::ScanResult;
using CandidateEntry = MeshCoP::Seeker::CandidateEntry;
static void CreateScanResult(ScanResult &aResult, uint64_t aExtPanId, uint64_t aExtAddr, int8_t aRssi)
{
ClearAllBytes(aResult);
LittleEndian::WriteUint64(aExtPanId, aResult.mExtendedPanId.m8);
LittleEndian::WriteUint64(aExtAddr, aResult.mExtAddress.m8);
aResult.mRssi = aRssi;
aResult.mPanId = static_cast<uint16_t>(aExtPanId & 0xffff);
aResult.mChannel = 11;
aResult.mJoinerUdpPort = 1000;
}
static void LogCandidate(const CandidateEntry &aEntry)
{
if (aEntry.IsEmpty())
{
printf(" empty\n");
}
else
{
printf(" ext-addr:%2.2s, ext-panid:%4.4s, rssi:%d, prf:%u, conn-attempted:%u\n",
aEntry.mExtAddr.ToString().AsCString(), aEntry.mExtPanId.ToString().AsCString(), aEntry.mRssi,
aEntry.mPreferred, aEntry.mConnAttempted);
}
}
static void LogCandidates(const Seeker &aSeeker)
{
CandidateEntry entry;
printf("\nCandidates:\n");
for (entry.InitForIteration(); aSeeker.mCandidates.ReadNext(entry) == kErrorNone;)
{
LogCandidate(entry);
}
printf("\n");
}
static void SaveCandidate(Seeker &aSeeker, uint64_t aExtPanId, uint64_t aExtAddr, int8_t aRssi, bool aPreferred)
{
ScanResult result;
CreateScanResult(result, aExtPanId, aExtAddr, aRssi);
aSeeker.SaveCandidate(result, aPreferred);
}
static bool Contains(const Seeker &aSeeker, uint64_t aExtPanId, uint64_t aExtAddr)
{
MeshCoP::ExtendedPanId extPanId;
Mac::ExtAddress extAddr;
CandidateEntry entry;
LittleEndian::WriteUint64(aExtPanId, extPanId.m8);
LittleEndian::WriteUint64(aExtAddr, extAddr.m8);
return (aSeeker.mCandidates.FindMatching(entry, extPanId, extAddr) == kErrorNone);
}
static void StartCandidateSelection(Seeker &aSeeker)
{
// Manually set the state so we can call and validate the
// `SelectNextCandidate()`.
aSeeker.SetState(Seeker::kStateConnectingNetworks);
}
static void SelectNextCandidate(Seeker &aSeeker, CandidateEntry &aEntry)
{
Error error = aSeeker.SelectNextCandidate(aEntry);
if (error == kErrorNone)
{
aEntry.mConnAttempted = true;
SuccessOrQuit(aSeeker.mCandidates.Write(aEntry));
}
else
{
aEntry.MarkAsEmpty();
}
}
static void CheckSelectionWith(Seeker &aSeeker, const uint64_t *aExtAddrs, uint16_t aNumExtAddrs)
{
CandidateEntry entry;
Mac::ExtAddress extAddr;
printf("\nSelection order:\n");
StartCandidateSelection(aSeeker);
for (uint16_t index = 0; index < aNumExtAddrs; index++)
{
SelectNextCandidate(aSeeker, entry);
LogCandidate(entry);
VerifyOrQuit(!entry.IsEmpty());
LittleEndian::WriteUint64(aExtAddrs[index], extAddr.m8);
VerifyOrQuit(entry.mExtAddr == extAddr);
VerifyOrQuit(entry.mConnAttempted);
}
SelectNextCandidate(aSeeker, entry);
VerifyOrQuit(entry.IsEmpty());
}
template <uint16_t kExtAddrSize>
static void CheckSelection(Seeker &aSeeker, const uint64_t (&aExtAddrArray)[kExtAddrSize])
{
CheckSelectionWith(aSeeker, &aExtAddrArray[0], kExtAddrSize);
}
static void TestSeekerCandidates(void)
{
Instance *instance;
printf("TestSeekerCandidates()\n");
instance = static_cast<Instance *>(testInitInstance());
VerifyOrQuit(instance != nullptr);
Seeker &seeker = instance->Get<Seeker>();
CandidateEntry entry;
printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n");
printf("Basic addition & replacement\n\n");
seeker.Stop();
printf("Save a single candidate");
SaveCandidate(seeker, 0xaaaa, 0xa1, -50, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 1);
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
printf("Save same candidate with better RSSI");
SaveCandidate(seeker, 0xaaaa, 0xa1, -40, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 1);
VerifyOrQuit(seeker.mCandidates.ReadAt(0, entry) == kErrorNone);
VerifyOrQuit(entry.mRssi == -40);
printf("Save same candidate with worse RSSI, still should replace as it is same extAddr\n");
SaveCandidate(seeker, 0xaaaa, 0xa1, -60, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 1);
VerifyOrQuit(seeker.mCandidates.ReadAt(0, entry) == kErrorNone);
VerifyOrQuit(entry.mRssi == -60);
printf("Validate candidate selection with single entry in array\n\n");
CheckSelection(seeker, {0xa1});
printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n");
printf("Max candidates per network (limit = 3)\n\n");
seeker.Stop();
printf("Save 3 candidates for network 0xaaaa along with some extra entries\n");
SaveCandidate(seeker, 0xaaaa, 0xa1, -50, false);
SaveCandidate(seeker, 0xbbbb, 0xb1, -70, true);
SaveCandidate(seeker, 0xaaaa, 0xa2, -52, false);
SaveCandidate(seeker, 0xcccc, 0xc1, -80, true);
SaveCandidate(seeker, 0xaaaa, 0xa3, -51, false);
SaveCandidate(seeker, 0xdddd, 0xd1, -40, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 6);
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa2));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
printf("Try adding 4th for 0xaaaa (worse RSSI) -> should be dropped\n");
SaveCandidate(seeker, 0xaaaa, 0xa4, -90, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 6);
VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa4));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa2));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
printf("Try adding 4th for 0xaaaa (better RSSI) -> should replace a2 (lowest RSSI)\n");
SaveCandidate(seeker, 0xaaaa, 0xa5, -40, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 6);
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa2));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n");
printf("Behavior under full candidates array and eviction\n\n");
SaveCandidate(seeker, 0xbbbb, 0xb2, -75, true);
SaveCandidate(seeker, 0xeeee, 0xe1, -30, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb2));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1));
printf("Try adding new entry 0xb3 for 0xbbbb with better RSSI -> should replace 0xb2\n");
SaveCandidate(seeker, 0xbbbb, 0xb3, -65, true);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(!Contains(seeker, 0xbbbb, 0xb2));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1));
printf("Try adding new entry 0xb4 for 0xbbbb with worst RSSI -> should be dropped\n");
SaveCandidate(seeker, 0xbbbb, 0xb4, -95, true);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1));
printf("Try adding new entry 0xc2 for 0xcccc with better RSSI but not preferred -> should be ignored\n");
SaveCandidate(seeker, 0xcccc, 0xc2, -40, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1));
printf("Try adding new entry 0xc3 for 0xcccc with better RSSI and preferred -> should replace 0xc1\n");
SaveCandidate(seeker, 0xcccc, 0xc3, -40, true);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3));
VerifyOrQuit(!Contains(seeker, 0xcccc, 0xc1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1));
printf("Try adding new entry 0xe2 for 0xeeee with worse RSSI but preferred -> should replace 0xe1\n");
SaveCandidate(seeker, 0xeeee, 0xe2, -99, true);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(!Contains(seeker, 0xeeee, 0xe1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
printf("Try adding new network, 0xf1 for 0xffff -> should evict 0xa3\n");
SaveCandidate(seeker, 0xffff, 0xf1, -65, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa3));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2));
VerifyOrQuit(Contains(seeker, 0xffff, 0xf1));
printf("Adding two new entries for new network -> should evict 0xa1 and 0xb1\n");
SaveCandidate(seeker, 0x1234, 0x01, -80, false);
SaveCandidate(seeker, 0x5678, 0x02, -70, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa1));
VerifyOrQuit(!Contains(seeker, 0xbbbb, 0xb1));
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2));
VerifyOrQuit(Contains(seeker, 0xffff, 0xf1));
VerifyOrQuit(Contains(seeker, 0x1234, 0x01));
VerifyOrQuit(Contains(seeker, 0x5678, 0x02));
printf("The candidates array is full and consists of distinct networks\n");
printf("Try adding a new entry for yet another network -> should be dropped\n");
SaveCandidate(seeker, 0xabcd, 0x03, -80, true);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
VerifyOrQuit(seeker.mCandidates.IsFull());
VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5));
VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3));
VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3));
VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1));
VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2));
VerifyOrQuit(Contains(seeker, 0xffff, 0xf1));
VerifyOrQuit(Contains(seeker, 0x1234, 0x01));
VerifyOrQuit(Contains(seeker, 0x5678, 0x02));
CheckSelection(seeker, {0xc3, 0xb3, 0xe2, 0xa5, 0xd1, 0xf1, 0x02, 0x01});
printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n");
printf("Selection strategy\n\n");
seeker.Stop();
SaveCandidate(seeker, 0xdddd, 0xd1, -30, false);
SaveCandidate(seeker, 0xaaaa, 0xa1, -60, false);
SaveCandidate(seeker, 0xeeee, 0xe1, -30, true);
SaveCandidate(seeker, 0xbbbb, 0xb1, -65, true);
SaveCandidate(seeker, 0xaaaa, 0xa2, -40, false);
SaveCandidate(seeker, 0xcccc, 0xc1, -90, true);
SaveCandidate(seeker, 0xaaaa, 0xa3, -70, false);
SaveCandidate(seeker, 0xcccc, 0xc2, -40, false);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 8);
// First we should go through all distinct networks, starting
// with most favored over all. Then go through the extra
// backup candidates.
//
// For `0xaaaa`, we have 3 candidates:
// ext-addr:a2, ext-panid:aaaa, rssi:-40, prf:0, conn-attempted:0
// ext-addr:a1, ext-panid:aaaa, rssi:-60, prf:0, conn-attempted:0
// ext-addr:a3, ext-panid:aaaa, rssi:-70, prf:0, conn-attempted:0
//
// For `0xbbbb`, only one candidate:
// ext-addr:b1, ext-panid:bbbb, rssi:-65, prf:1, conn-attempted:0
//
// For `0xcccc`, we have two:
// ext-addr:c1, ext-panid:cccc, rssi:-90, prf:1, conn-attempted:0
// ext-addr:c2, ext-panid:cccc, rssi:-40, prf:0, conn-attempted:0
//
// For `0xdddd`, we have one:
// ext-addr:d1, ext-panid:dddd, rssi:-30, prf:0, conn-attempted:0
//
// For `0xeeee`, we have one:
// ext-addr:e1, ext-panid:eeee, rssi:-30, prf:1, conn-attempted:0
//
// We go through networks first
// - e1 has highest RSSI and also preferred
// - b1 is preferred with high RSSI
// - c1 is also preferred even though it has low RSSI
// - d1 has best RSSI among non-preferred
// - a2 would be next among all `0xaaaa` candidates
//
// Next we go through remaining candidates
// - c2, a1 and a3
CheckSelection(seeker, {0xe1, 0xb1, 0xc1, 0xd1, 0xa2, 0xc2, 0xa1, 0xa3});
seeker.Stop();
// Adding two candidates for 3 networks (total 6)
SaveCandidate(seeker, 0xcccc, 0xc2, -92, true);
SaveCandidate(seeker, 0xaaaa, 0xa2, -76, true);
SaveCandidate(seeker, 0xbbbb, 0xb2, -56, false);
SaveCandidate(seeker, 0xbbbb, 0xb1, -55, false);
SaveCandidate(seeker, 0xcccc, 0xc1, -90, true);
SaveCandidate(seeker, 0xaaaa, 0xa1, -75, true);
LogCandidates(seeker);
VerifyOrQuit(seeker.mCandidates.GetLength() == 6);
CheckSelection(seeker, {0xa1, 0xc1, 0xb1, 0xa2, 0xc2, 0xb2});
printf("\nTestSeekerCandidates() passed\n\n");
testFreeInstance(instance);
}
};
#endif // #if OPENTHREAD_CONFIG_SEEKER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE
} // namespace ot
int main(void)
{
#if OPENTHREAD_CONFIG_SEEKER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE
#if (OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES == 8)
ot::UnitTester::TestSeekerCandidates();
printf("All tests passed\n");
#else
printf("Skipping tests as the test expects `OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES` to be `8`\n");
printf("This config is specifically set to 8 in `toranj-config` for this test\n");
#endif
#else
printf("Seeker feature is disabled, skipping the test\n");
#endif
return 0;
}