diff --git a/include/openthread/instance.h b/include/openthread/instance.h index b30b4dc60..30628563a 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 (628) +#define OPENTHREAD_API_VERSION (629) /** * @addtogroup api-instance diff --git a/include/openthread/thread.h b/include/openthread/thread.h index 2aea2d0d6..bf4211a16 100644 --- a/include/openthread/thread.h +++ b/include/openthread/thread.h @@ -714,6 +714,25 @@ otError otThreadBecomeChild(otInstance *aInstance); */ otError otThreadGetNextNeighborInfo(otInstance *aInstance, otNeighborInfoIterator *aIterator, otNeighborInfo *aInfo); +/** + * Gets the neighbor information for a neighbor with a given RLOC16. + * + * This is a direct lookup alternative to iterating the whole neighbor table with `otThreadGetNextNeighborInfo()` + * when the RLOC16 of the neighbor is already known (e.g., from the router table). + * + * Only neighbors in a valid state are considered. On a router or leader, the lookup covers children and neighboring + * routers. On a child, it covers the parent. On an FTD child, it also covers neighboring routers with which the child + * has established a link. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aRloc16 The RLOC16 of the neighbor. + * @param[out] aInfo A pointer to output the neighbor information. + * + * @retval OT_ERROR_NONE Successfully found the neighbor and retrieved its information in @p aInfo. + * @retval OT_ERROR_NOT_FOUND No valid neighbor with @p aRloc16 exists in the neighbor table. + */ +otError otThreadGetNeighborInfoByRloc16(otInstance *aInstance, uint16_t aRloc16, otNeighborInfo *aInfo); + /** * Get the device role. * diff --git a/src/cli/README.md b/src/cli/README.md index 9f4a2b0aa..9c032445e 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -3050,6 +3050,29 @@ Get the NAT64 translator packet and error counters. Done ``` +### neighbor \ + +Print information about the neighbor with the given RLOC16. + +For `Role`, the possible values are `C` (Child) or `R` (Router). `Age` and `Connection Time` are in seconds. + +```bash +> neighbor 0xc800 +Role: R +Rloc16: 0xc800 +Ext Addr: 9a91556102c39ddb +Age: 2 +Connection Time: 644 +Avg RSSI: -29 +Last RSSI: -29 +Link Quality In: 3 +Frame Error: 0.00 % +Msg Error: 0.00 % +Mode: rdn +Version: 5 +Done +``` + ### neighbor linkquality Print link quality info for all neighbors. diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index 885a29ec0..6a6b33506 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -4679,11 +4679,62 @@ template <> otError Interpreter::Process(Arg aArgs[]) } } } + /** + * @cli neighbor (rloc16) + * @code + * neighbor 0xc800 + * Role: R + * Rloc16: 0xc800 + * Ext Addr: 9a91556102c39ddb + * Age: 2 + * Connection Time: 644 + * Avg RSSI: -29 + * Last RSSI: -29 + * Link Quality In: 3 + * Frame Error: 0.00 % + * Msg Error: 0.00 % + * Mode: rdn + * Version: 5 + * Done + * @endcode + * @cparam neighbor @ca{rloc16} + * @par api_copy + * #otThreadGetNeighborInfoByRloc16 + * @par + * Prints information about the neighbor with the given RLOC16. + * @par + * For `Role`, the possible values are `C` (Child) or `R` (Router). `Age` and `Connection Time` are in seconds. + */ else { - error = OT_ERROR_INVALID_ARGS; + uint16_t rloc16; + PercentageStringBuffer stringBuffer; + otLinkModeConfig linkMode; + char linkModeString[kLinkModeStringSize]; + + SuccessOrExit(error = aArgs[0].ParseAsUint16(rloc16)); + VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS); + SuccessOrExit(error = otThreadGetNeighborInfoByRloc16(GetInstancePtr(), rloc16, &neighborInfo)); + + OutputLine("Role: %c", neighborInfo.mIsChild ? 'C' : 'R'); + OutputLine("Rloc16: 0x%04x", neighborInfo.mRloc16); + OutputFormat("Ext Addr: "); + OutputExtAddressLine(neighborInfo.mExtAddress); + OutputLine("Age: %lu", ToUlong(neighborInfo.mAge)); + OutputLine("Connection Time: %lu", ToUlong(neighborInfo.mConnectionTime)); + OutputLine("Avg RSSI: %d", neighborInfo.mAverageRssi); + OutputLine("Last RSSI: %d", neighborInfo.mLastRssi); + OutputLine("Link Quality In: %u", neighborInfo.mLinkQualityIn); + OutputLine("Frame Error: %s %%", PercentageToString(neighborInfo.mFrameErrorRate, stringBuffer)); + OutputLine("Msg Error: %s %%", PercentageToString(neighborInfo.mMessageErrorRate, stringBuffer)); + linkMode.mRxOnWhenIdle = neighborInfo.mRxOnWhenIdle; + linkMode.mDeviceType = neighborInfo.mFullThreadDevice; + linkMode.mNetworkData = neighborInfo.mFullNetworkData; + OutputLine("Mode: %s", LinkModeToString(linkMode, linkModeString)); + OutputLine("Version: %u", neighborInfo.mVersion); } +exit: return error; } #endif // OPENTHREAD_FTD diff --git a/src/core/api/thread_api.cpp b/src/core/api/thread_api.cpp index 8ab98376c..00d3dd0a4 100644 --- a/src/core/api/thread_api.cpp +++ b/src/core/api/thread_api.cpp @@ -272,6 +272,11 @@ otError otThreadGetNextNeighborInfo(otInstance *aInstance, otNeighborInfoIterato return AsCoreType(aInstance).Get().GetNextNeighborInfo(*aIterator, AsCoreType(aInfo)); } +otError otThreadGetNeighborInfoByRloc16(otInstance *aInstance, uint16_t aRloc16, otNeighborInfo *aInfo) +{ + return AsCoreType(aInstance).Get().GetNeighborInfo(aRloc16, AsCoreType(aInfo)); +} + otDeviceRole otThreadGetDeviceRole(otInstance *aInstance) { return MapEnum(AsCoreType(aInstance).Get().GetRole()); diff --git a/src/core/thread/neighbor_table.cpp b/src/core/thread/neighbor_table.cpp index 45fdca2ff..0628368f1 100644 --- a/src/core/thread/neighbor_table.cpp +++ b/src/core/thread/neighbor_table.cpp @@ -260,6 +260,48 @@ exit: #endif +Error NeighborTable::GetNeighborInfo(uint16_t aRloc16, Neighbor::Info &aNeighInfo) +{ + Error error = kErrorNone; + Neighbor *neighbor = nullptr; + + // `kShortAddrInvalid` acts as a wildcard in `AddressMatcher`, so it + // must be rejected before the child table lookup. + VerifyOrExit((aRloc16 != Mac::kShortAddrBroadcast) && (aRloc16 != Mac::kShortAddrInvalid), error = kErrorNotFound); + + if (Get().IsChild() && Get().GetParent().IsStateValid() && + (Get().GetParent().GetRloc16() == aRloc16)) + { + neighbor = &Get().GetParent(); + } +#if OPENTHREAD_FTD + else if (Get().IsRouterOrLeader() && Mle::IsChildRloc16(aRloc16)) + { + neighbor = Get().FindChild(aRloc16, Child::kInStateValid); + } + else if (Get().IsAttached() && Mle::IsRouterRloc16(aRloc16)) + { + // Neighboring routers on a router or leader, or router links + // on an FTD child. The router table is indexed by Router ID + // only, so the full RLOC16 is also checked. + Router *router = Get().FindRouterByRloc16(aRloc16); + + if ((router != nullptr) && router->IsStateValid() && (router->GetRloc16() == aRloc16)) + { + neighbor = router; + } + } +#endif + + VerifyOrExit(neighbor != nullptr, error = kErrorNotFound); + + aNeighInfo.SetFrom(*neighbor); + aNeighInfo.mIsChild = Mle::IsChildRloc16(aRloc16); + +exit: + return error; +} + void NeighborTable::Signal(Event aEvent, const Neighbor &aNeighbor) { #if !OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE diff --git a/src/core/thread/neighbor_table.hpp b/src/core/thread/neighbor_table.hpp index 7b95ee6fc..dde7eba7b 100644 --- a/src/core/thread/neighbor_table.hpp +++ b/src/core/thread/neighbor_table.hpp @@ -210,6 +210,17 @@ public: */ Error GetNextNeighborInfo(Iterator &aIterator, Neighbor::Info &aNeighInfo); + /** + * Gets the neighbor information for a valid neighbor with a given RLOC16. + * + * @param[in] aRloc16 The RLOC16 of the neighbor. + * @param[out] aNeighInfo The neighbor information. + * + * @retval kErrorNone Successfully found the neighbor and retrieved its information. + * @retval kErrorNotFound No valid neighbor with @p aRloc16 exists in the table. + */ + Error GetNeighborInfo(uint16_t aRloc16, Neighbor::Info &aNeighInfo); + /** * Registers the "neighbor table changed" callback function. * diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index b29a4f747..7f33ba5f7 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -435,6 +435,7 @@ ot_nexus_test(mle_blocking_downgrade "core;nexus") ot_nexus_test(mle_msg_key_seq_jump "core;nexus") ot_nexus_test(mlr_manager "core;nexus") ot_nexus_test(nat64_translator "core;nexus") +ot_nexus_test(neighbor_info_by_rloc16 "core;nexus") ot_nexus_test(netdata_publisher "core;nexus") ot_nexus_test(on_mesh_prefix "core;nexus") ot_nexus_test(pbbr_aloc "core;nexus") diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index c4afdbc45..dc1f1400c 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -239,6 +239,7 @@ DEFAULT_TESTS=( "router_reboot_multiple_link_request" "pbbr_aloc" "retransmission_security" + "neighbor_info_by_rloc16" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_neighbor_info_by_rloc16.cpp b/tests/nexus/test_neighbor_info_by_rloc16.cpp new file mode 100644 index 000000000..cc8fd2a8b --- /dev/null +++ b/tests/nexus/test_neighbor_info_by_rloc16.cpp @@ -0,0 +1,213 @@ +/* + * 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 + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +static void VerifyMatchesIteration(Node &aNode, const otNeighborInfo &aInfo) +{ + // The direct lookup must report the same entry as iterating the table. + + otNeighborInfoIterator iterator = OT_NEIGHBOR_INFO_ITERATOR_INIT; + otNeighborInfo iterInfo; + bool found = false; + + while (otThreadGetNextNeighborInfo(&aNode.GetInstance(), &iterator, &iterInfo) == OT_ERROR_NONE) + { + if (iterInfo.mRloc16 == aInfo.mRloc16) + { + VerifyOrQuit(!found); + VerifyOrQuit(AsCoreType(&iterInfo.mExtAddress) == AsCoreType(&aInfo.mExtAddress)); + VerifyOrQuit(iterInfo.mAge == aInfo.mAge); + VerifyOrQuit(iterInfo.mConnectionTime == aInfo.mConnectionTime); + VerifyOrQuit(iterInfo.mLinkFrameCounter == aInfo.mLinkFrameCounter); + VerifyOrQuit(iterInfo.mMleFrameCounter == aInfo.mMleFrameCounter); + VerifyOrQuit(iterInfo.mLinkQualityIn == aInfo.mLinkQualityIn); + VerifyOrQuit(iterInfo.mAverageRssi == aInfo.mAverageRssi); + VerifyOrQuit(iterInfo.mLastRssi == aInfo.mLastRssi); + VerifyOrQuit(iterInfo.mLinkMargin == aInfo.mLinkMargin); + VerifyOrQuit(iterInfo.mFrameErrorRate == aInfo.mFrameErrorRate); + VerifyOrQuit(iterInfo.mMessageErrorRate == aInfo.mMessageErrorRate); + VerifyOrQuit(iterInfo.mVersion == aInfo.mVersion); + VerifyOrQuit(iterInfo.mRxOnWhenIdle == aInfo.mRxOnWhenIdle); + VerifyOrQuit(iterInfo.mFullThreadDevice == aInfo.mFullThreadDevice); + VerifyOrQuit(iterInfo.mFullNetworkData == aInfo.mFullNetworkData); + VerifyOrQuit(iterInfo.mIsChild == aInfo.mIsChild); + found = true; + } + } + + VerifyOrQuit(found); +} + +static void VerifyCliRole(Node &aNode, uint16_t aRloc16, const char *aExpectedRoleLine) +{ + aNode.InputCli("neighbor 0x%04x", aRloc16); + VerifyOrQuit(aNode.IsCliOutputSuccess()); + VerifyOrQuit(aNode.GetCliOutputLines()[0].Matches(aExpectedRoleLine)); +} + +void TestNeighborInfoByRloc16(void) +{ + Core nexus; + otNeighborInfo info; + uint16_t router1Rloc16; + + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &med = nexus.CreateNode(); + Node &fed = nexus.CreateNode(); + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelInfo)); + + Log("---------------------------------------------------------------------------------------"); + Log("Form network with a router, an MED, and an FED"); + + leader.Form(); + nexus.AdvanceTime(13 * 1000); + VerifyOrQuit(leader.Get().IsLeader()); + + // The children join before `router1`, so the leader is the only + // parent candidate and `router1` is a non-parent router to them. + med.Join(leader, Node::kAsMed); + fed.Join(leader, Node::kAsFed); + nexus.AdvanceTime(20 * 1000); + + router1.Join(leader); + nexus.AdvanceTime(300 * 1000); + + VerifyOrQuit(router1.Get().IsRouter()); + VerifyOrQuit(med.Get().IsChild()); + VerifyOrQuit(med.Get().GetParent().GetRloc16() == leader.Get().GetRloc16()); + VerifyOrQuit(fed.Get().IsChild()); + VerifyOrQuit(fed.Get().GetParent().GetRloc16() == leader.Get().GetRloc16()); + + Log("---------------------------------------------------------------------------------------"); + Log("Leader: look up a neighboring router by RLOC16"); + + SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), router1.Get().GetRloc16(), &info)); + VerifyOrQuit(info.mRloc16 == router1.Get().GetRloc16()); + VerifyOrQuit(AsCoreType(&info.mExtAddress) == router1.Get().GetExtAddress()); + VerifyOrQuit(!info.mIsChild); + VerifyOrQuit(info.mFullThreadDevice); + VerifyMatchesIteration(leader, info); + VerifyCliRole(leader, router1.Get().GetRloc16(), "Role: R"); + + Log("---------------------------------------------------------------------------------------"); + Log("Leader: look up a child by RLOC16"); + + SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), med.Get().GetRloc16(), &info)); + VerifyOrQuit(info.mRloc16 == med.Get().GetRloc16()); + VerifyOrQuit(AsCoreType(&info.mExtAddress) == med.Get().GetExtAddress()); + VerifyOrQuit(info.mIsChild); + VerifyOrQuit(!info.mFullThreadDevice); + VerifyMatchesIteration(leader, info); + VerifyCliRole(leader, med.Get().GetRloc16(), "Role: C"); + + Log("---------------------------------------------------------------------------------------"); + Log("Router: look up the leader by RLOC16"); + + SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&router1.GetInstance(), leader.Get().GetRloc16(), &info)); + VerifyOrQuit(!info.mIsChild); + VerifyMatchesIteration(router1, info); + VerifyCliRole(router1, leader.Get().GetRloc16(), "Role: R"); + + Log("---------------------------------------------------------------------------------------"); + Log("Leader: unknown, unused-bit, broadcast, invalid, and own RLOC16 are not found"); + + // Child IDs are allocated from 1 upwards, so the highest one is unused. + VerifyOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), + leader.Get().GetRloc16() | Mle::kMaxChildId, + &info) == OT_ERROR_NOT_FOUND); + + // Bit 9 is not part of the Router ID or Child ID, so this must not + // match `router1` even though the router table is indexed by Router ID. + VerifyOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), router1.Get().GetRloc16() | 0x0200, + &info) == OT_ERROR_NOT_FOUND); + + VerifyOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), Mac::kShortAddrBroadcast, &info) == + OT_ERROR_NOT_FOUND); + VerifyOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), Mac::kShortAddrInvalid, &info) == + OT_ERROR_NOT_FOUND); + VerifyOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), leader.Get().GetRloc16(), &info) == + OT_ERROR_NOT_FOUND); + + Log("---------------------------------------------------------------------------------------"); + Log("MED: parent is found, non-parent router is not"); + + SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&med.GetInstance(), leader.Get().GetRloc16(), &info)); + VerifyOrQuit(AsCoreType(&info.mExtAddress) == leader.Get().GetExtAddress()); + VerifyOrQuit(!info.mIsChild); + VerifyCliRole(med, leader.Get().GetRloc16(), "Role: R"); + + // Not cross-checked against iteration: the FTD build of `otThreadGetNextNeighborInfo()` + // does not report the parent while in child role (the MTD build does). + + VerifyOrQuit(otThreadGetNeighborInfoByRloc16(&med.GetInstance(), router1.Get().GetRloc16(), &info) == + OT_ERROR_NOT_FOUND); + + Log("---------------------------------------------------------------------------------------"); + Log("FED: the parent and any non-parent router with an established link are found"); + + SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&fed.GetInstance(), leader.Get().GetRloc16(), &info)); + VerifyOrQuit(AsCoreType(&info.mExtAddress) == leader.Get().GetExtAddress()); + VerifyOrQuit(!info.mIsChild); + + // An FTD child establishes links with neighboring routers other than + // its parent, and these must be reported just as iteration does. + SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&fed.GetInstance(), router1.Get().GetRloc16(), &info)); + VerifyOrQuit(AsCoreType(&info.mExtAddress) == router1.Get().GetExtAddress()); + VerifyOrQuit(!info.mIsChild); + VerifyMatchesIteration(fed, info); + + Log("---------------------------------------------------------------------------------------"); + Log("Leader: router that has left is no longer found"); + + router1Rloc16 = router1.Get().GetRloc16(); + router1.Get().Stop(); + nexus.AdvanceTime(300 * 1000); + + VerifyOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), router1Rloc16, &info) == OT_ERROR_NOT_FOUND); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestNeighborInfoByRloc16(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/expect/cli-neighbor.exp b/tests/scripts/expect/cli-neighbor.exp index 4325f3c5a..444d6ed5c 100755 --- a/tests/scripts/expect/cli-neighbor.exp +++ b/tests/scripts/expect/cli-neighbor.exp @@ -45,6 +45,22 @@ expect_line "Done" send "neighbor list\n" expect "0x$rloc" expect_line "Done" +send "neighbor 0x$rloc\n" +expect "Role: C" +expect "Rloc16: 0x$rloc" +expect "Ext Addr: $extaddr" +expect -re "Connection Time: \\d+" +expect_line "Mode: r" +expect_line "Done" +send "neighbor 0xfffe\n" +expect "Error 23: NotFound" +set leader_rloc [get_rloc16] +switch_node 2 +send "neighbor 0x$leader_rloc\n" +expect "Role: R" +expect "Rloc16: 0x$leader_rloc" +expect_line "Done" +switch_node 1 send "neighbor something_invalid\n" expect "Error 7: InvalidArgs"