diff --git a/include/openthread/instance.h b/include/openthread/instance.h index d2e754a04..55bab0c7c 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (462) +#define OPENTHREAD_API_VERSION (463) /** * @addtogroup api-instance diff --git a/include/openthread/thread.h b/include/openthread/thread.h index 0769952fe..a9ff1e222 100644 --- a/include/openthread/thread.h +++ b/include/openthread/thread.h @@ -176,6 +176,7 @@ typedef struct otMleCounters uint16_t mAttachAttempts; ///< Number of attach attempts while device was detached. uint16_t mPartitionIdChanges; ///< Number of changes to partition ID. uint16_t mBetterPartitionAttachAttempts; ///< Number of attempts to attach to a better partition. + uint16_t mBetterParentAttachAttempts; ///< Number of attempts to attach to find a better parent (parent search). /** * Role time tracking. diff --git a/src/cli/README.md b/src/cli/README.md index ad8dd647d..25701df4c 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -1069,6 +1069,7 @@ Role Leader: 1 Attach Attempts: 1 Partition Id Changes: 1 Better Partition Attach Attempts: 0 +Better Parent Attach Attempts: 0 Parent Changes: 0 Time Disabled Milli: 10026 Time Detached Milli: 6852 diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index bef5ad5ca..5ecd2247d 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -2308,6 +2308,7 @@ template <> otError Interpreter::Process(Arg aArgs[]) * Attach Attempts: 1 * Partition Id Changes: 1 * Better Partition Attach Attempts: 0 + * Better Parent Attach Attempts: 0 * Parent Changes: 0 * Done * @endcode @@ -2334,6 +2335,7 @@ template <> otError Interpreter::Process(Arg aArgs[]) {&otMleCounters::mAttachAttempts, "Attach Attempts"}, {&otMleCounters::mPartitionIdChanges, "Partition Id Changes"}, {&otMleCounters::mBetterPartitionAttachAttempts, "Better Partition Attach Attempts"}, + {&otMleCounters::mBetterParentAttachAttempts, "Better Parent Attach Attempts"}, {&otMleCounters::mParentChanges, "Parent Changes"}, }; diff --git a/src/core/config/parent_search.h b/src/core/config/parent_search.h index 9430f31ef..b5a821979 100644 --- a/src/core/config/parent_search.h +++ b/src/core/config/parent_search.h @@ -46,21 +46,46 @@ /** * @def OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE * - * Define as 1 to enable periodic parent search feature. + * Define as 1 to enable the periodic parent search feature. * - * When this feature is enabled an end-device/child (while staying attached) will periodically search for a possible - * better parent and will switch parent if a better one is found. * - * The child will periodically check the average RSS value for the current parent, and only if it is below a specific - * threshold, a parent search is performed. The `OPENTHREAD_CONFIG_PARENT_SEARCH_CHECK_INTERVAL` specifies the - * check interval (in seconds) and `OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_THRESHOLD` gives the RSS threshold. + * When this feature is enabled, an end device/child (while staying attached) periodically searches for a potentially + * better parent and switches parents if a better one is found. * - * Since the parent search process can be power consuming (child needs to stays in RX mode to collect parent response) - * and to limit its impact on battery-powered devices, after a parent search is triggered, the child will not trigger - * another one before a specified backoff interval specified by `OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL`. + * The parent search mechanism depends on whether the device is an FTD child or an MTD child. + * + * FTD Child + * + * - An FTD child receives and processes MLE Advertisements from neighboring routers. It uses this information to track + * the one-way link quality to each, which is later used to compare and select potential new parents. + * - Every `OPENTHREAD_CONFIG_PARENT_SEARCH_CHECK_INTERVAL` seconds, an FTD child tries to select a better parent. + * The FTD child checks the list of neighboring routers and the tracked link quality information. A new parent is + * selected only if its average RSS exceeds the current parent's RSS by a margin specified by the configuration + * `OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_MARGIN` configuration. + * - If the attach attempt to the selected router fails (e.g., the router already has the maximum number of children it + * can support), the FTD child ensures that the same router cannot be selected again until a "reselect timeout" + * expires. This avoids repeated attempts to the same router. This timeout is specified by the configuration + * `OPENTHREAD_CONFIG_PARENT_SEARCH_RESELECT_TIMEOUT`. + * + * MTD Child + * + * - Every `OPENTHREAD_CONFIG_PARENT_SEARCH_CHECK_INTERVAL` seconds, an MTD child checks its average RSS to its + * current parent. The child starts a parent search process only if the average RSS is below + * `OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_THRESHOLD`. + * - This ensures that an MTD child already attached to a parent with good link quality does not waste energy + * searching for better parents. + * - The MTD child sends an MLE Parent Request to discover possible new parents. Because this process can be + * power-consuming (the child needs to stay in RX mode to collect parent responses), and to limit its impact on + * battery-powered devices, after a parent search is triggered on an MTD, the MTD child does not trigger another + * one before the specified backoff interval (`OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL`) expires. + * + * This feature is enabled by default on FTD builds. It is recommended that it also be enabled on MTD builds. This may + * require the platform integrator (device vendor) to select appropriate configuration values for this feature, + * particularly `OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL`, which can impact how often a (battery-powered) + * sleepy child may search for a parent, taking into account its impact on the device's battery life. */ #ifndef OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE -#define OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE 0 +#define OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE OPENTHREAD_FTD #endif /** @@ -77,7 +102,8 @@ /** * @def OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL * - * Specifies the backoff interval in seconds for a child to not perform a parent search after triggering one. + * Specifies the backoff interval in seconds for a child to not perform a parent search after triggering one. This is + * used when device is an MTD child. * * Applicable only if periodic parent search feature is enabled (see `OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE`). */ @@ -97,6 +123,31 @@ #endif /** + * @def OPENTHREAD_CONFIG_PARENT_SEARCH_RESELECT_TIMEOUT + * + * Specifies parent reselect timeout duration in seconds used on FTD child devices. When an attach attempt to a + * neighboring router selected as a potential new parent fails, the same router cannot be selected again until this + * timeout expires. + * + * Applicable only if periodic parent search feature is enabled (see `OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE`). + * + */ +#ifndef OPENTHREAD_CONFIG_PARENT_SEARCH_RESELECT_TIMEOUT +#define OPENTHREAD_CONFIG_PARENT_SEARCH_RESELECT_TIMEOUT (90 * 60) +#endif + +/** + * @def OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_MARGIN + * + * Specifies the RSS margin over the current parent RSS for allowing selection of a neighboring router as a potential + * new parent to attach to. Used on FTD child devices. + * + */ +#ifndef OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_MARGIN +#define OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_MARGIN 7 +#endif + +/* * @} */ diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp index 342e3f340..a975e654d 100644 --- a/src/core/thread/mle.cpp +++ b/src/core/thread/mle.cpp @@ -126,7 +126,7 @@ Error Mle::Enable(void) SuccessOrExit(error = mSocket.Bind(kUdpPort)); #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE - mParentSearch.StartTimer(); + mParentSearch.SetEnabled(true); #endif exit: return error; @@ -1310,6 +1310,13 @@ Error Mle::DetermineParentRequestType(ParentRequestType &aType) const OT_ASSERT(mAttachState == kAttachStateParentRequest); + if (mAttachMode == kSelectedParent) + { + aType = kToSelectedRouter; + VerifyOrExit(mParentRequestCounter <= 1, error = kErrorNotFound); + ExitNow(); + } + aType = kToRoutersAndReeds; // If device is not yet attached, `mAttachCounter` will track the @@ -1378,14 +1385,23 @@ bool Mle::HasAcceptableParentCandidate(void) const if (IsChild()) { - // If already attached, accept the parent candidate if - // we are trying to attach to a better partition or if a - // Parent Response was also received from the current parent - // to which the device is attached. This ensures that the - // new parent candidate is compared with the current parent - // and that it is indeed preferred over the current one. + switch (mAttachMode) + { + case kBetterPartition: + break; - VerifyOrExit(mReceivedResponseFromParent || (mAttachMode == kBetterPartition)); + case kAnyPartition: + case kSamePartition: + case kDowngradeToReed: + case kBetterParent: + case kSelectedParent: + // Ensure that a Parent Response was received from the + // current parent to which the device is attached, so + // that the new parent candidate can be compared with the + // current parent and confirmed to be preferred. + VerifyOrExit(mReceivedResponseFromParent); + break; + } } hasAcceptableParent = true; @@ -1451,7 +1467,18 @@ void Mle::HandleAttachTimer(void) if (DetermineParentRequestType(type) == kErrorNone) { SendParentRequest(type); - delay = (type == kToRouters) ? kParentRequestRouterTimeout : kParentRequestReedTimeout; + + switch (type) + { + case kToRouters: + case kToSelectedRouter: + delay = kParentRequestRouterTimeout; + break; + case kToRoutersAndReeds: + delay = kParentRequestReedTimeout; + break; + } + break; } @@ -1552,6 +1579,7 @@ uint32_t Mle::Reattach(void) { case kAnyPartition: case kBetterParent: + case kSelectedParent: if (!IsChild()) { if (mAlternatePanId != Mac::kPanIdBroadcast) @@ -1606,6 +1634,7 @@ void Mle::SendParentRequest(ParentRequestType aType) switch (aType) { case kToRouters: + case kToSelectedRouter: scanMask = ScanMaskTlv::kRouterFlag; break; @@ -1623,12 +1652,38 @@ void Mle::SendParentRequest(ParentRequestType aType) SuccessOrExit(error = message->AppendTimeRequestTlv()); #endif - destination.SetToLinkLocalAllRoutersMulticast(); +#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE + if (aType == kToSelectedRouter) + { + TxMessage *messageToCurParent = static_cast(message->Clone()); + + VerifyOrExit(messageToCurParent != nullptr, error = kErrorNoBufs); + + destination.SetToLinkLocalAddress(mParent.GetExtAddress()); + error = messageToCurParent->SendTo(destination); + + if (error != kErrorNone) + { + messageToCurParent->Free(); + ExitNow(); + } + + Log(kMessageSend, kTypeParentRequestToRouters, destination); + + destination.SetToLinkLocalAddress(mParentSearch.GetSelectedParent().GetExtAddress()); + } + else +#endif + { + destination.SetToLinkLocalAllRoutersMulticast(); + } + SuccessOrExit(error = message->SendTo(destination)); switch (aType) { case kToRouters: + case kToSelectedRouter: Log(kMessageSend, kTypeParentRequestToRouters, destination); break; @@ -3034,7 +3089,6 @@ void Mle::HandleParentResponse(RxInfo &aRxInfo) switch (mAttachMode) { case kAnyPartition: - case kBetterParent: VerifyOrExit(!isPartitionIdSame || isIdSequenceGreater); break; @@ -3052,6 +3106,10 @@ void Mle::HandleParentResponse(RxInfo &aRxInfo) VerifyOrExit(MleRouter::ComparePartitions(connectivityTlv.IsSingleton(), leaderData, Get().IsSingleton(), mLeaderData) > 0); break; + + case kBetterParent: + case kSelectedParent: + break; } } #endif @@ -3794,9 +3852,20 @@ exit: #endif // OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE + +void Mle::ParentSearch::SetEnabled(bool aEnabled) +{ + VerifyOrExit(mEnabled != aEnabled); + mEnabled = aEnabled; + StartTimer(); + +exit: + return; +} + void Mle::ParentSearch::HandleTimer(void) { - int8_t parentRss; + AttachMode attachMode; LogInfo("PeriodicParentSearch: %s interval passed", mIsInBackoff ? "Backoff" : "Check"); @@ -3817,25 +3886,95 @@ void Mle::ParentSearch::HandleTimer(void) VerifyOrExit(Get().IsChild()); - parentRss = Get().GetParent().GetLinkInfo().GetAverageRss(); - LogInfo("PeriodicParentSearch: Parent RSS %d", parentRss); - VerifyOrExit(parentRss != Radio::kInvalidRssi); - - if (parentRss < kRssThreshold) +#if OPENTHREAD_FTD + if (Get().IsFullThreadDevice()) { - LogInfo("PeriodicParentSearch: Parent RSS less than %d, searching for new parents", kRssThreshold); - mIsInBackoff = true; - Get().Attach(kBetterParent); + SuccessOrExit(SelectBetterParent()); + attachMode = kSelectedParent; } + else +#endif + { + int8_t parentRss; + + parentRss = Get().GetParent().GetLinkInfo().GetAverageRss(); + LogInfo("PeriodicParentSearch: Parent RSS %d", parentRss); + VerifyOrExit(parentRss != Radio::kInvalidRssi); + + VerifyOrExit(parentRss < kRssThreshold); + LogInfo("PeriodicParentSearch: Parent RSS less than %d, searching for new parents", kRssThreshold); + + mIsInBackoff = true; + attachMode = kBetterParent; + } + + Get().mCounters.mBetterParentAttachAttempts++; + Get().Attach(attachMode); exit: StartTimer(); } +#if OPENTHREAD_FTD +Error Mle::ParentSearch::SelectBetterParent(void) +{ + Error error = kErrorNone; + + mSelectedParent = nullptr; + + for (Router &router : Get()) + { + CompareAndUpdateSelectedParent(router); + } + + VerifyOrExit(mSelectedParent != nullptr, error = kErrorNotFound); + mSelectedParent->SetParentReselectTimeout(kParentReselectTimeout); + + LogInfo("PeriodicParentSearch: Selected router 0x%04x as parent with RSS %d", mSelectedParent->GetRloc16(), + mSelectedParent->GetLinkInfo().GetAverageRss()); + +exit: + return error; +} + +void Mle::ParentSearch::CompareAndUpdateSelectedParent(Router &aRouter) +{ + int8_t routerRss; + + VerifyOrExit(aRouter.IsSelectableAsParent()); + VerifyOrExit(aRouter.GetParentReselectTimeout() == 0); + VerifyOrExit(aRouter.GetRloc16() != Get().GetParent().GetRloc16()); + + routerRss = aRouter.GetLinkInfo().GetAverageRss(); + VerifyOrExit(routerRss != Radio::kInvalidRssi); + + if (mSelectedParent == nullptr) + { + VerifyOrExit(routerRss >= Get().GetParent().GetLinkInfo().GetAverageRss() + kRssMarginOverParent); + } + else + { + VerifyOrExit(routerRss > mSelectedParent->GetLinkInfo().GetAverageRss()); + } + + mSelectedParent = &aRouter; + +exit: + return; +} + +#endif // OPENTHREAD_FTD + void Mle::ParentSearch::StartTimer(void) { uint32_t interval; + if (!mEnabled) + { + mTimer.Stop(); + ExitNow(); + } + interval = Random::NonCrypto::GetUint32InRange(0, kJitterInterval); if (mIsInBackoff) @@ -3850,6 +3989,9 @@ void Mle::ParentSearch::StartTimer(void) mTimer.Start(interval); LogInfo("PeriodicParentSearch: (Re)starting timer for %s interval", mIsInBackoff ? "backoff" : "check"); + +exit: + return; } void Mle::ParentSearch::UpdateState(void) @@ -4102,6 +4244,7 @@ const char *Mle::AttachModeToString(AttachMode aMode) "BetterPartition", // (2) kBetterPartition "DowngradeToReed", // (3) kDowngradeToReed "BetterParent", // (4) kBetterParent + "SelectedParent", // (5) kSelectedParent }; struct EnumCheck @@ -4112,6 +4255,7 @@ const char *Mle::AttachModeToString(AttachMode aMode) ValidateNextEnum(kBetterPartition); ValidateNextEnum(kDowngradeToReed); ValidateNextEnum(kBetterParent); + ValidateNextEnum(kSelectedParent); }; return kAttachModeStrings[aMode]; diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp index 4476bf6aa..3de579ed0 100644 --- a/src/core/thread/mle.hpp +++ b/src/core/thread/mle.hpp @@ -807,6 +807,7 @@ private: kBetterPartition, // Attach to a better (i.e. higher weight/partition id) Thread partition. kDowngradeToReed, // Attach to the same Thread partition during downgrade process. kBetterParent, // Attach to a better parent. + kSelectedParent, // Attach to a selected parent. }; enum AttachState : uint8_t @@ -857,6 +858,7 @@ private: { kToRouters, // Parent Request to routers only. kToRoutersAndReeds, // Parent Request to all routers and REEDs. + kToSelectedRouter, // Parent Request to a selected router (e.g., by `ParentSearch` module). }; enum ChildUpdateRequestMode : uint8_t // Used in `SendChildUpdateRequest()` @@ -1215,6 +1217,7 @@ private: public: explicit ParentSearch(Instance &aInstance) : InstanceLocator(aInstance) + , mEnabled(false) , mIsInBackoff(false) , mBackoffWasCanceled(false) , mRecentlyDetached(false) @@ -1223,10 +1226,14 @@ private: { } - void StartTimer(void); + void SetEnabled(bool aEnabled); + bool IsEnabled(void) const { return mEnabled; } void UpdateState(void); void SetRecentlyDetached(void) { mRecentlyDetached = true; } void HandleTimer(void); +#if OPENTHREAD_FTD + const Neighbor &GetSelectedParent(void) const { return *mSelectedParent; } +#endif private: // All timer intervals are converted to milliseconds. @@ -1235,13 +1242,26 @@ private: static constexpr uint32_t kJitterInterval = (15 * 1000u); static constexpr int8_t kRssThreshold = OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_THRESHOLD; +#if OPENTHREAD_FTD + static constexpr int8_t kRssMarginOverParent = OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_MARGIN; + static constexpr uint16_t kParentReselectTimeout = OPENTHREAD_CONFIG_PARENT_SEARCH_RESELECT_TIMEOUT; // in sec + + Error SelectBetterParent(void); + void CompareAndUpdateSelectedParent(Router &aRouter); +#endif + void StartTimer(void); + using SearchTimer = TimerMilliIn; + bool mEnabled : 1; bool mIsInBackoff : 1; bool mBackoffWasCanceled : 1; bool mRecentlyDetached : 1; TimeMilli mBackoffCancelTime; SearchTimer mTimer; +#if OPENTHREAD_FTD + Router *mSelectedParent; +#endif }; #endif // OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE diff --git a/src/core/thread/mle_router.cpp b/src/core/thread/mle_router.cpp index 98bd36481..55e987663 100644 --- a/src/core/thread/mle_router.cpp +++ b/src/core/thread/mle_router.cpp @@ -375,7 +375,7 @@ void MleRouter::HandleChildStart(AttachMode aMode) case kAnyPartition: case kBetterParent: - + case kSelectedParent: // If attach was initiated due to receiving an MLE Announce // message, all rx-on-when-idle devices will immediately // attempt to attach as well. This aligns with the Thread 1.1 @@ -1311,6 +1311,10 @@ Error MleRouter::HandleAdvertisementOnFtd(RxInfo &aRxInfo, uint16_t aSourceAddre } } +#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE + router->SetSelectableAsParent(true); +#endif + router->SetLastHeard(TimerMilli::GetNow()); ExitNow(); @@ -1664,6 +1668,15 @@ void MleRouter::HandleTimeTick(void) } } +#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE + router.DecrementParentReselectTimeout(); + + if (age >= kMaxNeighborAge) + { + router.SetSelectableAsParent(false); + } +#endif + if (router.IsStateLinkRequest() && (age >= kLinkRequestTimeout)) { LogInfo("Router 0x%04x - Link Request timeout expired", router.GetRloc16()); diff --git a/src/core/thread/router.hpp b/src/core/thread/router.hpp index 8ff8741b3..aaa4706fe 100644 --- a/src/core/thread/router.hpp +++ b/src/core/thread/router.hpp @@ -140,6 +140,42 @@ public: */ bool SetNextHopToInvalid(void); +#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE + /** + * Indicates whether or not this router can be selected as parent. + * + * @retval TRUE The router is selectable as parent. + * @retval FALSE The router is not selectable as parent. + */ + bool IsSelectableAsParent(void) const { return mIsSelectableAsParent; } + + /** + * Sets whether or not this router is selectable as parent. + * + * @param[in] aIsSelectable Boolean indicating whether or not router is selectable as parent. + */ + void SetSelectableAsParent(bool aIsSelectable) { mIsSelectableAsParent = aIsSelectable; } + + /** + * Sets timeout duration in seconds to block reselecting this router as parent. + * + * @param[in] aTimeout The timeout duration in seconds. + */ + void SetParentReselectTimeout(uint16_t aTimeout) { mParentReselectTimeout = aTimeout; } + + /** + * Gets the remaining timeout duration in seconds to block reselecting this router parent. + * + * @returns The remaining timeout duration in seconds. + */ + uint16_t GetParentReselectTimeout(void) const { return mParentReselectTimeout; } + + /** + * Decrements the reselect timeout duration (if non-zero). + */ + void DecrementParentReselectTimeout(void) { (mParentReselectTimeout > 0) ? mParentReselectTimeout-- : 0; } +#endif + private: uint8_t mNextHop; ///< The next hop towards this router uint8_t mLinkQualityOut : 2; ///< The link quality out for this router @@ -149,6 +185,10 @@ private: #else uint8_t mCost : 4; ///< The cost to this router via neighbor router #endif +#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE + bool mIsSelectableAsParent : 1; + uint16_t mParentReselectTimeout; +#endif }; /** diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py index d86d46550..921f5eb43 100644 --- a/tests/toranj/cli/cli.py +++ b/tests/toranj/cli/cli.py @@ -366,6 +366,12 @@ class Node(object): def get_nexthop(self, rloc16): return self._cli_single_output('nexthop', rloc16) + def get_child_max(self): + return self._cli_single_output('childmax') + + def set_child_max(self, childmax): + self._cli_no_output('childmax', childmax) + def get_parent_info(self): outputs = self.cli('parent') result = {} @@ -891,6 +897,15 @@ class Node(object): """Removes a given node (of node `Node) from the allowlist""" self._cli_no_output('macfilter addr remove', node.get_ext_addr()) + def denylist_node(self, node): + """Adds a given node to the denylist of `self` and enables denylisting on `self`""" + self._cli_no_output('macfilter addr add', node.get_ext_addr()) + self._cli_no_output('macfilter addr denylist') + + def un_denylist_node(self, node): + """Removes a given node (of node `Node) from the denylist""" + self._cli_no_output('macfilter addr remove', node.get_ext_addr()) + def set_macfilter_lqi_to_node(self, node, lqi): self._cli_no_output('macfilter rss add-lqi', node.get_ext_addr(), lqi) diff --git a/tests/toranj/cli/test-034-fed-parent-search.py b/tests/toranj/cli/test-034-fed-parent-search.py new file mode 100755 index 000000000..8d8c57942 --- /dev/null +++ b/tests/toranj/cli/test-034-fed-parent-search.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Validate parent search on FTD child (FED/REED). +# +# Topology: +# +# The link between `r1` and `fed` is configured to be poor, ensuring +# `fed` will search for a new parent. `r2` is configured with +# `set_child_max(1)`, allowing only one child. `c2` is already +# attached to `r2`, preventing `r2` from accepting `fed` as a child. +# Later, `r3` is added to the network, and the test validates that +# `fed` successfully switches to `r3` as its parent. +# +# +# r1 ---- r2 r3 -- r1 -- r2 +# . | \ | +# . | ==> \ | +# fed c2 fed c2 + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 40 +cli.Node.set_time_speedup_factor(speedup) + +fed = cli.Node() +r1 = cli.Node() +r2 = cli.Node() +c2 = cli.Node() +r3 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +r1.set_macfilter_lqi_to_node(fed, 1) +fed.set_macfilter_lqi_to_node(r1, 1) + +r1.allowlist_node(fed) +r1.allowlist_node(r2) +r1.allowlist_node(r3) + +r2.allowlist_node(fed) +r2.allowlist_node(r1) +r2.allowlist_node(c2) +r2.allowlist_node(r3) + +c2.allowlist_node(r2) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# `r2` is allowed to have one child only (`c2`) + +r2.set_child_max(1) +verify(int(r2.get_child_max()) == 1) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Form the network. + +r1.form('fed-prnt-srch') +fed.join(r1, cli.JOIN_TYPE_REED) +r2.join(r1) +c2.join(r2, cli.JOIN_TYPE_END_DEVICE) + +verify(r1.get_state() == 'leader') +verify(fed.get_state() == 'child') +verify(r2.get_state() == 'router') +verify(c2.get_state() == 'child') + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Check that `c2` is attached to `r2`. + +info = c2.get_parent_info() +verify(int(info['Rloc'], 16) == int(r2.get_rloc16(), 16)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Check that `fed` is attached to `r1` and has poor link quality. + +info = fed.get_parent_info() +verify(int(info['Rloc'], 16) == int(r1.get_rloc16(), 16)) +verify(int(info['Link Quality In']) == 1) +verify(int(info['Link Quality Out']) == 1) + +verify(int(cli.Node.parse_list(fed.get_mle_counter())['Better Parent Attach Attempts']) == 0) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# The `toranj` config sets `PARENT_SEARCH_CHECK_INTERVAL` to 120 seconds. + +time.sleep(130 / speedup) + +# Verify that parent search mechanism did trigger an attach attempt on +# `fed`. + +verify(int(cli.Node.parse_list(fed.get_mle_counter())['Better Parent Attach Attempts']) == 1) + +# Since `r2` can have one child only and already has `c2`, the attach +# attempt should fail and `fed` should still have `r1` as its +# parent. + +info = fed.get_parent_info() +verify(int(info['Rloc'], 16) == int(r1.get_rloc16(), 16)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Ensure that `fed` does not initiate any further attach attempts, as +# the only available parent (`r2`) should be in the "reselect timeout" +# period. This should prevent `fed` from attempting to attach to `r2`. + +time.sleep(150 / speedup) + +verify(int(cli.Node.parse_list(fed.get_mle_counter())['Better Parent Attach Attempts']) == 1) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Add `r3` to the network and wait for `PARENT_SEARCH_CHECK_INTERVAL`. +# This should trigger a parent search on `fed`. Validate that +# `fed` successfully switches to `r3` as its new parent. + +r3.join(r1) +verify(r3.get_state() == 'router') + +time.sleep(130 / speedup) + +counters = cli.Node.parse_list(fed.get_mle_counter()) +verify(int(counters['Better Parent Attach Attempts']) == 2) +verify(int(counters['Parent Changes']) == 1) + +info = fed.get_parent_info() +verify(int(info['Rloc'], 16) == int(r3.get_rloc16(), 16)) +verify(int(info['Link Quality In']) == 3) +verify(int(info['Link Quality Out']) == 3) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index bb0f4a458..53bae9f8f 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -152,6 +152,10 @@ #define OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE 1 +#define OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE 1 + +#define OPENTHREAD_CONFIG_PARENT_SEARCH_CHECK_INTERVAL 120 + #define OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE 1 #define OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE 1 diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index ee1c74b4e..3d9add9c3 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -198,6 +198,7 @@ if [ "$TORANJ_CLI" = 1 ]; then run cli/test-031-service-aloc-route-lookup.py run cli/test-032-leader-take-over.py run cli/test-033-alt-short-addr-role-transition.py + run cli/test-034-fed-parent-search.py run cli/test-035-context-id-change-addr-reg.py run cli/test-400-srp-client-server.py run cli/test-401-srp-server-address-cache-snoop.py