diff --git a/src/core/config/joiner.h b/src/core/config/joiner.h index 55d19f119..3b17757ff 100644 --- a/src/core/config/joiner.h +++ b/src/core/config/joiner.h @@ -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 /** diff --git a/src/core/meshcop/seeker.cpp b/src/core/meshcop/seeker.cpp index d384f6f0f..faebe86e6 100644 --- a/src/core/meshcop/seeker.cpp +++ b/src/core/meshcop/seeker.cpp @@ -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().Discover(Mac::ChannelMask(0), Get().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().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(reinterpret_cast(end - 1) - reinterpret_cast(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(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(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().SetPanId(candidate->mPanId); - SuccessOrExit(error = Get().SetPanChannel(candidate->mChannel)); + Get().SetPanId(entry.mPanId); + SuccessOrExit(error = Get().SetPanChannel(entry.mChannel)); if (!Get().IsUnsecurePort(mUdpPort)) { SuccessOrExit(error = Get().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 diff --git a/src/core/meshcop/seeker.hpp b/src/core/meshcop/seeker.hpp index 5ef1b0bda..c4a0754b5 100644 --- a/src/core/meshcop/seeker.hpp +++ b/src/core/meshcop/seeker.hpp @@ -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 { - 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; + + 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 mScanEvaluator; - Candidate mCandidates[kMaxCandidates]; - uint16_t mCandidateIndex; + CandidateArray mCandidates; }; } // namespace MeshCoP diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h index f60a2d61c..9e08676fa 100644 --- a/tests/toranj/openthread-core-toranj-config-simulation.h +++ b/tests/toranj/openthread-core-toranj-config-simulation.h @@ -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 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 85abe38c1..f613f5c1f 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -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) diff --git a/tests/unit/test_seeker.cpp b/tests/unit/test_seeker.cpp new file mode 100644 index 000000000..8dd2d1629 --- /dev/null +++ b/tests/unit/test_seeker.cpp @@ -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(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 + 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(testInitInstance()); + VerifyOrQuit(instance != nullptr); + + Seeker &seeker = instance->Get(); + 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; +}