[joiner] introduce Seeker to handle discovery and candidate tracking (#12325)

This change introduces a new `Seeker` class to encapsulate the logic
for discovering and prioritizing Joiner Router candidates.

The `Joiner` class is updated to use the `Seeker` to perform the
discovery scan. The `Joiner` provides a callback `EvaluateScanResult`
to the `Seeker` to filter and evaluate scan results based on
Steering Data, preserving the existing behavior.

This change separates the discovery mechanism from the `Joiner` state
machine. This simplifies the `Joiner` implementation and facilitates
future enhancements to the joining process.

This commit makes to changes to how the Joiner Router candidates are
tracked or prioritized.
This commit is contained in:
Abtin Keshavarzian
2026-02-04 12:51:57 -08:00
committed by GitHub
parent 7577311aa4
commit 848918df32
8 changed files with 548 additions and 184 deletions
+2
View File
@@ -586,6 +586,8 @@ openthread_core_files = [
"meshcop/panid_query_client.hpp",
"meshcop/secure_transport.cpp",
"meshcop/secure_transport.hpp",
"meshcop/seeker.cpp",
"meshcop/seeker.hpp",
"meshcop/steering_data.cpp",
"meshcop/steering_data.hpp",
"meshcop/tcat_agent.cpp",
+1
View File
@@ -182,6 +182,7 @@ set(COMMON_SOURCES
meshcop/network_name.cpp
meshcop/panid_query_client.cpp
meshcop/secure_transport.cpp
meshcop/seeker.cpp
meshcop/steering_data.cpp
meshcop/tcat_agent.cpp
meshcop/timestamp.cpp
+1
View File
@@ -188,6 +188,7 @@ Instance::Instance(void)
, mTmfSecureAgent(*this)
#endif
#if OPENTHREAD_CONFIG_JOINER_ENABLE
, mSeeker(*this)
, mJoiner(*this)
#endif
#if OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE
+4
View File
@@ -99,6 +99,7 @@
#include "meshcop/joiner_router.hpp"
#include "meshcop/meshcop_leader.hpp"
#include "meshcop/network_identity.hpp"
#include "meshcop/seeker.hpp"
#include "net/dhcp6_client.hpp"
#include "net/dhcp6_server.hpp"
#include "net/dhcp6_types.hpp"
@@ -618,6 +619,7 @@ private:
#endif
#if OPENTHREAD_CONFIG_JOINER_ENABLE
MeshCoP::Seeker mSeeker;
MeshCoP::Joiner mJoiner;
#endif
@@ -958,6 +960,8 @@ template <> inline Dnssd &Instance::Get(void) { return mDnssd; }
#endif
#if OPENTHREAD_CONFIG_JOINER_ENABLE
template <> inline MeshCoP::Seeker &Instance::Get(void) { return mSeeker; }
template <> inline MeshCoP::Joiner &Instance::Get(void) { return mJoiner; }
#endif
+72 -147
View File
@@ -45,13 +45,11 @@ RegisterLogModule("Joiner");
Joiner::Joiner(Instance &aInstance)
: InstanceLocator(aInstance)
, mState(kStateIdle)
, mJoinerRouterIndex(0)
, mFinalizeMessage(nullptr)
, mTimer(aInstance)
{
SetIdFromIeeeEui64();
mDiscerner.Clear();
ClearAllBytes(mJoinerRouters);
}
void Joiner::SetIdFromIeeeEui64(void)
@@ -113,10 +111,9 @@ Error Joiner::Start(const char *aPskd,
otJoinerCallback aCallback,
void *aContext)
{
Error error;
JoinerPskd joinerPskd;
Mac::ExtAddress randomAddress;
SteeringData::HashBitIndexes filterIndexes;
Error error;
bool shouldCleanup = false;
JoinerPskd joinerPskd;
LogInfo("Joiner starting");
@@ -124,51 +121,36 @@ Error Joiner::Start(const char *aPskd,
SuccessOrExit(error = Tlv::ValidateStringValue<VendorNameTlv>(aVendorName));
SuccessOrExit(error = Tlv::ValidateStringValue<VendorModelTlv>(aVendorModel));
SuccessOrExit(error = Tlv::ValidateStringValue<VendorSwVersionTlv>(aVendorSwVersion));
VerifyOrExit(mState == kStateIdle, error = kErrorBusy);
VerifyOrExit(Get<ThreadNetif>().IsUp() && Get<Mle::Mle>().GetRole() == Mle::kRoleDisabled,
error = kErrorInvalidState);
SuccessOrExit(error = joinerPskd.SetFrom(aPskd));
randomAddress.GenerateRandom();
Get<Mac::Mac>().SetExtAddress(randomAddress);
Get<Mle::Mle>().UpdateLinkLocalAddress();
VerifyOrExit(mState == kStateIdle, error = kErrorBusy);
VerifyOrExit(!Get<Seeker>().IsRunning(), error = kErrorBusy);
SuccessOrExit(error = Get<Tmf::SecureAgent>().Open(Ip6::NetifIdentifier::kNetifThreadInternal));
SuccessOrExit(error = Get<Tmf::SecureAgent>().Bind(kJoinerUdpPort));
// After this, if any of the steps fails, we need to cleanup
// (free allocated message, stop seeker, close agent, etc).
shouldCleanup = true;
SuccessOrExit(error = Get<Tmf::SecureAgent>().Bind(Seeker::kUdpPort));
Get<Tmf::SecureAgent>().SetConnectCallback(HandleSecureCoapClientConnect, this);
Get<Tmf::SecureAgent>().SetPsk(joinerPskd);
for (JoinerRouter &router : mJoinerRouters)
{
router.mPriority = 0; // Priority zero means entry is not in-use.
}
SuccessOrExit(error = PrepareJoinerFinalizeMessage(aProvisioningUrl, aVendorName, aVendorModel, aVendorSwVersion,
aVendorData));
if (!mDiscerner.IsEmpty())
{
SteeringData::CalculateHashBitIndexes(mDiscerner, filterIndexes);
}
else
{
SetIdFromIeeeEui64();
SteeringData::CalculateHashBitIndexes(mId, filterIndexes);
}
SuccessOrExit(error = Get<Mle::DiscoverScanner>().Discover(Mac::ChannelMask(0), Get<Mac::Mac>().GetPanId(),
/* aJoiner */ true, /* aEnableFiltering */ true,
&filterIndexes, HandleDiscoverResult, this));
mCompletionCallback.Set(aCallback, aContext);
SetState(kStateDiscover);
error = Get<Seeker>().Start(EvaluateScanResult, this);
exit:
if (error != kErrorNone)
if ((error != kErrorNone) && shouldCleanup)
{
FreeJoinerFinalizeMessage();
Get<Tmf::SecureAgent>().Close();
Get<Seeker>().Stop();
SetState(kStateIdle);
}
LogWarnOnError(error, "start joiner");
@@ -196,13 +178,13 @@ void Joiner::Finish(Error aError)
case kStateEntrust:
case kStateJoined:
Get<Tmf::SecureAgent>().Disconnect();
IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
mTimer.Stop();
OT_FALL_THROUGH;
case kStateDiscover:
Get<Tmf::SecureAgent>().Close();
Get<Seeker>().Stop();
break;
}
@@ -215,114 +197,74 @@ exit:
return;
}
uint8_t Joiner::CalculatePriority(int8_t aRssi, bool aSteeringDataAllowsAny)
Seeker::Verdict Joiner::EvaluateScanResult(void *aContext, const Seeker::ScanResult *aResult)
{
int16_t priority;
return static_cast<Joiner *>(aContext)->EvaluateScanResult(aResult);
}
if (aRssi == Radio::kInvalidRssi)
Seeker::Verdict Joiner::EvaluateScanResult(const Seeker::ScanResult *aResult)
{
Seeker::Verdict verdict = Seeker::kIgnore;
const SteeringData *steeringData;
if (aResult == nullptr)
{
aRssi = -127;
HandleScanCompleted();
ExitNow();
}
priority = Clamp<int8_t>(aRssi, -127, -1);
steeringData = AsCoreTypePtr(&aResult->mSteeringData);
// Assign higher priority to networks with an exact match of Joiner
// ID in the Steering Data (128 < priority < 256) compared to ones
// that allow all Joiners (0 < priority < 128). Sub-prioritize
// based on signal strength. Priority 0 is reserved for unused
// entry.
// We prefer networks with an exact match of Joiner ID or
// Discerner in the Steering Data compared to ones that allow all
// Joiners.
priority += aSteeringDataAllowsAny ? 128 : 256;
return static_cast<uint8_t>(priority);
}
void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult, void *aContext)
{
static_cast<Joiner *>(aContext)->HandleDiscoverResult(aResult);
}
void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult)
{
VerifyOrExit(mState == kStateDiscover);
if (aResult != nullptr)
if (steeringData->PermitsAllJoiners())
{
SaveDiscoveredJoinerRouter(*aResult);
verdict = Seeker::kAccept;
ExitNow();
}
if (!mDiscerner.IsEmpty())
{
VerifyOrExit(steeringData->Contains(mDiscerner));
}
else
{
Get<Mac::Mac>().SetExtAddress(mId);
Get<Mle::Mle>().UpdateLinkLocalAddress();
mJoinerRouterIndex = 0;
TryNextJoinerRouter(kErrorNone);
VerifyOrExit(steeringData->Contains(mId));
}
verdict = Seeker::kAcceptPreferred;
exit:
return verdict;
}
void Joiner::HandleScanCompleted(void)
{
VerifyOrExit(mState == kStateDiscover);
Get<Mac::Mac>().SetExtAddress(mId);
Get<Mle::Mle>().UpdateLinkLocalAddress();
TryNextCandidate(kErrorNone);
exit:
return;
}
void Joiner::SaveDiscoveredJoinerRouter(const Mle::DiscoverScanner::ScanResult &aResult)
void Joiner::TryNextCandidate(Error aPrevError)
{
uint8_t priority;
bool doesAllowAny;
JoinerRouter *end;
JoinerRouter *entry;
Error error;
VerifyOrExit(aResult.mJoinerUdpPort > 0);
doesAllowAny = AsCoreType(&aResult.mSteeringData).PermitsAllJoiners();
LogInfo("Joiner discover network: %s, pan:0x%04x, port:%d, chan:%d, rssi:%d, allow-any:%s",
AsCoreType(&aResult.mExtAddress).ToString().AsCString(), aResult.mPanId, aResult.mJoinerUdpPort,
aResult.mChannel, aResult.mRssi, ToYesNo(doesAllowAny));
priority = CalculatePriority(aResult.mRssi, doesAllowAny);
// We keep the list sorted based on priority. Find the place to
// add the new result.
end = GetArrayEnd(mJoinerRouters);
for (entry = &mJoinerRouters[0]; entry < end; entry++)
do
{
if (priority > entry->mPriority)
error = ConnectToNextCandidate();
if (error == kErrorNone)
{
break;
ExitNow();
}
}
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;
}
void Joiner::TryNextJoinerRouter(Error aPrevError)
{
for (; mJoinerRouterIndex < GetArrayLength(mJoinerRouters); mJoinerRouterIndex++)
{
JoinerRouter &router = mJoinerRouters[mJoinerRouterIndex];
Error error;
if (router.mPriority == 0)
{
break;
}
error = Connect(router);
VerifyOrExit(error != kErrorNone, mJoinerRouterIndex++);
// Save the error from `Connect` only if there is no previous
// error from earlier attempts. This ensures that if there has
@@ -331,16 +273,9 @@ void Joiner::TryNextJoinerRouter(Error aPrevError)
// emitted from `Finish()` call corresponds to the error from
// that attempt.
if (aPrevError == kErrorNone)
{
aPrevError = error;
}
}
aPrevError = (aPrevError == kErrorNone) ? error : aPrevError;
if (aPrevError == kErrorNone)
{
aPrevError = kErrorNotFound;
}
} while (error != kErrorNotFound);
Finish(aPrevError);
@@ -348,26 +283,16 @@ exit:
return;
}
Error Joiner::Connect(JoinerRouter &aRouter)
Error Joiner::ConnectToNextCandidate(void)
{
Error error = kErrorNotFound;
Ip6::SockAddr sockAddr(aRouter.mJoinerUdpPort);
LogInfo("Joiner connecting to %s, pan:0x%04x, chan:%d", aRouter.mExtAddr.ToString().AsCString(), aRouter.mPanId,
aRouter.mChannel);
Get<Mac::Mac>().SetPanId(aRouter.mPanId);
SuccessOrExit(error = Get<Mac::Mac>().SetPanChannel(aRouter.mChannel));
SuccessOrExit(error = Get<Ip6::Filter>().AddUnsecurePort(kJoinerUdpPort));
sockAddr.GetAddress().SetToLinkLocalAddress(aRouter.mExtAddr);
Error error;
Ip6::SockAddr sockAddr;
SuccessOrExit(error = Get<Seeker>().SetUpNextConnection(sockAddr));
SuccessOrExit(error = Get<Tmf::SecureAgent>().Connect(sockAddr));
SetState(kStateConnect);
exit:
LogWarnOnError(error, "start secure joiner connection");
return error;
}
@@ -388,7 +313,7 @@ void Joiner::HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent)
}
else
{
TryNextJoinerRouter(kErrorSecurity);
TryNextCandidate(kErrorSecurity);
}
exit:
@@ -491,7 +416,7 @@ void Joiner::HandleJoinerFinalizeResponse(Coap::Msg *aMsg, Error aResult)
exit:
Get<Tmf::SecureAgent>().Disconnect();
IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
Get<Seeker>().Stop();
}
template <> void Joiner::HandleTmf<kUriJoinerEntrust>(Coap::Msg &aMsg)
+23 -37
View File
@@ -52,7 +52,7 @@
#include "meshcop/meshcop.hpp"
#include "meshcop/meshcop_tlvs.hpp"
#include "meshcop/secure_transport.hpp"
#include "thread/discover_scanner.hpp"
#include "meshcop/seeker.hpp"
#include "thread/tmf.hpp"
namespace ot {
@@ -135,7 +135,7 @@ public:
const Mac::ExtAddress &GetId(void) const { return mId; }
/**
* Gets the Jointer Discerner.
* Gets the Joiner Discerner.
*
* @returns A pointer to the current Joiner Discerner or `nullptr` if none is set.
*/
@@ -178,40 +178,28 @@ public:
static const char *StateToString(State aState);
private:
static constexpr uint16_t kMaxJoinerRouterCandidates = OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES;
static constexpr uint16_t kJoinerUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT;
static constexpr uint32_t kConfigExtAddressDelay = 100; // in msec.
static constexpr uint32_t kResponseTimeout = 4000; // in msec
static constexpr uint32_t kConfigExtAddressDelay = 100; // in msec.
static constexpr uint32_t kResponseTimeout = 4000; // in msec
struct JoinerRouter
{
Mac::ExtAddress mExtAddr;
Mac::PanId mPanId;
uint16_t mJoinerUdpPort;
uint8_t mChannel;
uint8_t mPriority;
};
void SetState(State aState);
void SetIdFromIeeeEui64(void);
void SaveDiscoveredJoinerRouter(const Mle::DiscoverScanner::ScanResult &aResult);
void TryNextJoinerRouter(Error aPrevError);
Error Connect(JoinerRouter &aRouter);
void Finish(Error aError);
void HandleTimer(void);
uint8_t CalculatePriority(int8_t aRssi, bool aSteeringDataAllowsAny);
Error PrepareJoinerFinalizeMessage(const char *aProvisioningUrl,
const char *aVendorName,
const char *aVendorModel,
const char *aVendorSwVersion,
const char *aVendorData);
void FreeJoinerFinalizeMessage(void);
void SendJoinerFinalize(void);
void SendJoinerEntrustResponse(const Coap::Msg &aMsg);
static void HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult, void *aContext);
void HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult);
static void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent, void *aContext);
void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent);
void SetState(State aState);
void SetIdFromIeeeEui64(void);
void TryNextCandidate(Error aPrevError);
Error ConnectToNextCandidate(void);
void Finish(Error aError);
void HandleTimer(void);
Error PrepareJoinerFinalizeMessage(const char *aProvisioningUrl,
const char *aVendorName,
const char *aVendorModel,
const char *aVendorSwVersion,
const char *aVendorData);
void FreeJoinerFinalizeMessage(void);
void SendJoinerFinalize(void);
void SendJoinerEntrustResponse(const Coap::Msg &aMsg);
static Seeker::Verdict EvaluateScanResult(void *aContext, const Seeker::ScanResult *aResult);
Seeker::Verdict EvaluateScanResult(const Seeker::ScanResult *aResult);
void HandleScanCompleted(void);
static void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent, void *aContext);
void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent);
DeclareTmfResponseHandlerIn(Joiner, HandleJoinerFinalizeResponse);
@@ -223,8 +211,6 @@ private:
JoinerDiscerner mDiscerner;
State mState;
Callback<CompletionCallback> mCompletionCallback;
JoinerRouter mJoinerRouters[kMaxJoinerRouterCandidates];
uint16_t mJoinerRouterIndex;
Coap::Message *mFinalizeMessage;
JoinerTimer mTimer;
};
+249
View File
@@ -0,0 +1,249 @@
/*
* 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.
*/
/**
* @file
* This file implements the Seeker functionality.
*/
#include "seeker.hpp"
#if OPENTHREAD_CONFIG_JOINER_ENABLE
#include "instance/instance.hpp"
namespace ot {
namespace MeshCoP {
RegisterLogModule("Seeker");
Seeker::Seeker(Instance &aInstance)
: InstanceLocator(aInstance)
, mState(kStateStopped)
, mCandidateIndex(0)
{
}
Error Seeker::Start(ScanEvaluator aScanEvaluator, void *aContext)
{
Error error = kErrorNone;
Mac::ExtAddress randomAddress;
VerifyOrExit(aScanEvaluator != nullptr, error = kErrorInvalidArgs);
VerifyOrExit(GetState() == kStateStopped, error = kErrorBusy);
VerifyOrExit(Get<ThreadNetif>().IsUp() && Get<Mle::Mle>().IsDisabled(), error = kErrorInvalidState);
randomAddress.GenerateRandom();
Get<Mac::Mac>().SetExtAddress(randomAddress);
Get<Mle::Mle>().UpdateLinkLocalAddress();
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,
/* aFilterIndexes */ nullptr, HandleDiscoverResult, this));
SetState(kStateDiscovering);
exit:
return error;
}
void Seeker::Stop(void)
{
switch (GetState())
{
case kStateStopped:
case kStateDiscovering:
case kStateDiscoverDone:
break;
case kStateConnecting:
IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kUdpPort));
break;
}
SetState(kStateStopped);
}
void Seeker::HandleDiscoverResult(ScanResult *aResult, void *aContext)
{
static_cast<Seeker *>(aContext)->HandleDiscoverResult(aResult);
}
void Seeker::HandleDiscoverResult(ScanResult *aResult)
{
bool preferred = false;
VerifyOrExit(GetState() == kStateDiscovering);
if (aResult == nullptr)
{
SetState(kStateDiscoverDone);
IgnoreReturnValue(mScanEvaluator.Invoke(aResult));
ExitNow();
}
VerifyOrExit(aResult->mJoinerUdpPort > 0);
VerifyOrExit(AsCoreType(&aResult->mSteeringData).IsValid());
switch (mScanEvaluator.Invoke(aResult))
{
case kAccept:
break;
case kAcceptPreferred:
preferred = true;
break;
case kIgnore:
default:
ExitNow();
}
SaveCandidate(*aResult, preferred);
exit:
return;
}
void Seeker::SaveCandidate(const ScanResult &aResult, bool aPreferred)
{
uint8_t priority;
Candidate *end;
Candidate *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));
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 (priority > entry->mPriority)
{
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);
}
Error Seeker::SetUpNextConnection(Ip6::SockAddr &aSockAddr)
{
Error error = kErrorNone;
const Candidate *candidate;
switch (GetState())
{
case kStateDiscoverDone:
case kStateConnecting:
break;
case kStateStopped:
case kStateDiscovering:
ExitNow(error = kErrorInvalidState);
}
candidate = &mCandidates[mCandidateIndex];
if (!candidate->IsValid())
{
Stop();
ExitNow(error = kErrorNotFound);
}
mCandidateIndex++;
LogInfo("Setting up conn to %s, pan:0x%04x, chan:%u", candidate->mExtAddr.ToString().AsCString(), candidate->mPanId,
candidate->mChannel);
Get<Mac::Mac>().SetPanId(candidate->mPanId);
SuccessOrExit(error = Get<Mac::Mac>().SetPanChannel(candidate->mChannel));
if (!Get<Ip6::Filter>().IsUnsecurePort(kUdpPort))
{
SuccessOrExit(error = Get<Ip6::Filter>().AddUnsecurePort(kUdpPort));
}
SetState(kStateConnecting);
aSockAddr.Clear();
aSockAddr.SetPort(candidate->mJoinerUdpPort);
aSockAddr.GetAddress().SetToLinkLocalAddress(candidate->mExtAddr);
exit:
return error;
}
} // namespace MeshCoP
} // namespace ot
#endif // OPENTHREAD_CONFIG_JOINER_ENABLE
+196
View File
@@ -0,0 +1,196 @@
/*
* 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.
*/
/**
* @file
* This file includes definitions for the Seeker module.
*
* The Seeker is responsible for discovering nearby Joiner Router candidates, prioritizing them, and
* iterating through the list to select the next best candidate for connection. It acts as a
* sub-module of the `Joiner` class.
*/
#ifndef OT_CORE_MESHCOP_SEEKER_HPP_
#define OT_CORE_MESHCOP_SEEKER_HPP_
#include "openthread-core-config.h"
#if OPENTHREAD_CONFIG_JOINER_ENABLE
#include "common/callback.hpp"
#include "common/error.hpp"
#include "common/locator.hpp"
#include "common/non_copyable.hpp"
#include "mac/mac_types.hpp"
#include "net/socket.hpp"
#include "thread/discover_scanner.hpp"
namespace ot {
namespace MeshCoP {
/**
* Represents a MeshCoP Seeker.
*/
class Seeker : public InstanceLocator, private NonCopyable
{
public:
static constexpr uint16_t kUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT; ///< The default Joiner UDP port.
typedef Mle::DiscoverScanner::ScanResult ScanResult; ///< Discover Scan result.
/**
* Represents a verdict returned from `ScanEvaluator` when evaluating a Discover Scan result.
*/
enum Verdict : uint8_t
{
kAccept, ///< The scan result is acceptable.
kAcceptPreferred, ///< The scan result is acceptable and preferred.
kIgnore, ///< The scan result should be ignored.
};
/**
* Represents the callback function type used to evaluate a scan result or report the end of a scan.
*
* @param[in] aContext A pointer to the callback context.
* @param[in] aResult A pointer to the scan result to evaluate, or `nullptr` to indicate scan completion.
*
* @returns The verdict for the scan result (`kAccept`, `kAcceptPreferred`, or `kIgnore`).
* If @p aResult is `nullptr` (scan complete), the return value is ignored.
*/
typedef Verdict (*ScanEvaluator)(void *aContext, const ScanResult *aResult);
/**
* Initializes the `Seeker`
*
* @param[in] aInstance The OpenThread instance.
*/
explicit Seeker(Instance &aInstance);
/**
* Starts the Seeker operation.
*
* The Seeker generates and sets a random MAC address for anonymity, then initiates an MLE Discover Scan to find
* Joiner Router candidates.
*
* Found candidates are reported to the @p aScanEvaluator callback. Based on the returned `Verdict`, the Seeker
* maintains a prioritized list of candidates for future connection attempts.
*
* @param[in] aScanEvaluator The callback function to evaluate scan results.
* @param[in] aContext An arbitrary context pointer to use with @p aScanEvaluator.
*
* @retval kErrorNone Successfully started the Seeker.
* @retval kErrorBusy The Seeker is already active (scanning or connecting).
* @retval kErrorInvalidState The IPv6 interface is not enabled, or MLE is enabled.
*/
Error Start(ScanEvaluator aScanEvaluator, void *aContext);
/**
* Stops the Seeker operation.
*
* This method stops any ongoing discovery or connection process, unregisters the unsecure Joiner UDP port, and
* clears internal state. If the Seeker is already stopped, this method has no effect.
*
* If the join process succeeds after a call to `SetupNextConnection()`, the caller MUST call this method to stop
* the Seeker and, importantly, unregister the Joiner UDP port.
*
* Note: If `SetupNextConnection()` returns `kErrorNotFound` (indicating the candidate list is exhausted), the
* Seeker stops automatically.
*/
void Stop(void);
/**
* Indicates whether or not the Seeker is running.
*
* @retval TRUE The seeker is active and running.
* @retval FALSE The seeker is stopped.
*/
bool IsRunning(void) const { return GetState() != kStateStopped; }
/**
* Selects the next best candidate and prepares the connection.
*
* This method must be called after the discovery scan has completed (indicated by the `ScanEvaluator` callback
* receiving `nullptr`). Calling it before scan completion will result in `kErrorInvalidState`.
*
* This method iterates through the discovered Joiner Router candidates in order of priority. For the selected
* candidate, it configures the radio channel and PAN ID, and populates @p aSockAddr with the candidate's address.
* It also registers the Joiner UDP port `kUdpPort` as an unsecure port to allow UDP
* connection to the candidate.
*
* If the list is exhausted, this method returns `kErrorNotFound` and automatically calls `Stop()`, which removes
* the unsecure port and clears internal state.
*
* @param[out] aSockAddr A reference to a socket address to output the candidate's address.
*
* @retval kErrorNone Successfully set up the connection to the next candidate.
* @retval kErrorNotFound No more candidates are available (list exhausted).
* @retval kErrorInvalidState The Seeker is not in a valid state (e.g. scan not yet completed).
*/
Error SetUpNextConnection(Ip6::SockAddr &aSockAddr);
private:
static constexpr uint16_t kMaxCandidates = OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES;
enum State : uint8_t
{
kStateStopped,
kStateDiscovering,
kStateDiscoverDone,
kStateConnecting,
};
struct Candidate
{
bool IsValid(void) const { return mPriority != 0; }
Mac::ExtAddress mExtAddr;
Mac::PanId mPanId;
uint16_t mJoinerUdpPort;
uint8_t mChannel;
uint8_t mPriority;
};
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);
State mState;
Callback<ScanEvaluator> mScanEvaluator;
Candidate mCandidates[kMaxCandidates];
uint16_t mCandidateIndex;
};
} // namespace MeshCoP
} // namespace ot
#endif // OPENTHREAD_CONFIG_JOINER_ENABLE
#endif // OT_CORE_MESHCOP_SEEKER_HPP_