[neighbor-table] add otThreadGetNeighborInfoByRloc16() API (#13715)

This commit adds `otThreadGetNeighborInfoByRloc16()`, which returns
the `otNeighborInfo` for a single neighbor given its RLOC16, along with
a matching `neighbor <rloc16>` CLI command.

Previously, the only way to get average/last RSSI and frame/message
error rates for a neighboring router was to iterate the whole table
with `otThreadGetNextNeighborInfo()`, since `otThreadGetRouterInfo()`
does not provide these fields. The new API is a direct lookup that
works for any neighbor type (child, router, or parent).

`NeighborTable::GetNeighborInfo()` rejects broadcast and invalid
RLOC16s, then checks the parent directly on a child. On a router or
leader, it looks up a child RLOC16 in the child table. On a router,
leader, or FTD child, it looks up a router RLOC16 in the router table,
requiring the full RLOC16 to match since the table is indexed by Router
ID only. On an FTD child, this covers routers with which the child has
established a link. Unlike the FTD build of
`otThreadGetNextNeighborInfo()`, the new API reports the parent when
the device is a child.

The CLI `neighbor <rloc16>` command prints role (`C`/`R`), RLOC16,
extended address, age, connection time, RSSI, link quality, error
rates, mode, and version.

This commit also adds the Nexus test `test_neighbor_info_by_rloc16`,
included in `DEFAULT_TESTS`, which covers routers, children, the
parent, an FED's link with a non-parent router, and not-found cases,
and cross-checks every field against the iterator. It also extends
`cli-neighbor.exp` with the new command.
This commit is contained in:
Tom Rebbert
2026-10-21 22:44:41 -07:00
committed by GitHub
parent 0592b760e2
commit 023d2e0762
11 changed files with 384 additions and 2 deletions
+1 -1
View File
@@ -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
+19
View File
@@ -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.
*
+23
View File
@@ -3050,6 +3050,29 @@ Get the NAT64 translator packet and error counters.
Done
```
### neighbor \<rloc16\>
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.
+52 -1
View File
@@ -4679,11 +4679,62 @@ template <> otError Interpreter::Process<Cmd("neighbor")>(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
+5
View File
@@ -272,6 +272,11 @@ otError otThreadGetNextNeighborInfo(otInstance *aInstance, otNeighborInfoIterato
return AsCoreType(aInstance).Get<NeighborTable>().GetNextNeighborInfo(*aIterator, AsCoreType(aInfo));
}
otError otThreadGetNeighborInfoByRloc16(otInstance *aInstance, uint16_t aRloc16, otNeighborInfo *aInfo)
{
return AsCoreType(aInstance).Get<NeighborTable>().GetNeighborInfo(aRloc16, AsCoreType(aInfo));
}
otDeviceRole otThreadGetDeviceRole(otInstance *aInstance)
{
return MapEnum(AsCoreType(aInstance).Get<Mle::Mle>().GetRole());
+42
View File
@@ -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<Mle::Mle>().IsChild() && Get<Mle::Mle>().GetParent().IsStateValid() &&
(Get<Mle::Mle>().GetParent().GetRloc16() == aRloc16))
{
neighbor = &Get<Mle::Mle>().GetParent();
}
#if OPENTHREAD_FTD
else if (Get<Mle::Mle>().IsRouterOrLeader() && Mle::IsChildRloc16(aRloc16))
{
neighbor = Get<ChildTable>().FindChild(aRloc16, Child::kInStateValid);
}
else if (Get<Mle::Mle>().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<RouterTable>().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
+11
View File
@@ -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.
*
+1
View File
@@ -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")
+1
View File
@@ -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
@@ -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 <stdio.h>
#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<Mle::Mle>().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<Mle::Mle>().IsRouter());
VerifyOrQuit(med.Get<Mle::Mle>().IsChild());
VerifyOrQuit(med.Get<Mle::Mle>().GetParent().GetRloc16() == leader.Get<Mle::Mle>().GetRloc16());
VerifyOrQuit(fed.Get<Mle::Mle>().IsChild());
VerifyOrQuit(fed.Get<Mle::Mle>().GetParent().GetRloc16() == leader.Get<Mle::Mle>().GetRloc16());
Log("---------------------------------------------------------------------------------------");
Log("Leader: look up a neighboring router by RLOC16");
SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), router1.Get<Mle::Mle>().GetRloc16(), &info));
VerifyOrQuit(info.mRloc16 == router1.Get<Mle::Mle>().GetRloc16());
VerifyOrQuit(AsCoreType(&info.mExtAddress) == router1.Get<Mac::Mac>().GetExtAddress());
VerifyOrQuit(!info.mIsChild);
VerifyOrQuit(info.mFullThreadDevice);
VerifyMatchesIteration(leader, info);
VerifyCliRole(leader, router1.Get<Mle::Mle>().GetRloc16(), "Role: R");
Log("---------------------------------------------------------------------------------------");
Log("Leader: look up a child by RLOC16");
SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&leader.GetInstance(), med.Get<Mle::Mle>().GetRloc16(), &info));
VerifyOrQuit(info.mRloc16 == med.Get<Mle::Mle>().GetRloc16());
VerifyOrQuit(AsCoreType(&info.mExtAddress) == med.Get<Mac::Mac>().GetExtAddress());
VerifyOrQuit(info.mIsChild);
VerifyOrQuit(!info.mFullThreadDevice);
VerifyMatchesIteration(leader, info);
VerifyCliRole(leader, med.Get<Mle::Mle>().GetRloc16(), "Role: C");
Log("---------------------------------------------------------------------------------------");
Log("Router: look up the leader by RLOC16");
SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&router1.GetInstance(), leader.Get<Mle::Mle>().GetRloc16(), &info));
VerifyOrQuit(!info.mIsChild);
VerifyMatchesIteration(router1, info);
VerifyCliRole(router1, leader.Get<Mle::Mle>().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<Mle::Mle>().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<Mle::Mle>().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<Mle::Mle>().GetRloc16(), &info) ==
OT_ERROR_NOT_FOUND);
Log("---------------------------------------------------------------------------------------");
Log("MED: parent is found, non-parent router is not");
SuccessOrQuit(otThreadGetNeighborInfoByRloc16(&med.GetInstance(), leader.Get<Mle::Mle>().GetRloc16(), &info));
VerifyOrQuit(AsCoreType(&info.mExtAddress) == leader.Get<Mac::Mac>().GetExtAddress());
VerifyOrQuit(!info.mIsChild);
VerifyCliRole(med, leader.Get<Mle::Mle>().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<Mle::Mle>().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<Mle::Mle>().GetRloc16(), &info));
VerifyOrQuit(AsCoreType(&info.mExtAddress) == leader.Get<Mac::Mac>().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<Mle::Mle>().GetRloc16(), &info));
VerifyOrQuit(AsCoreType(&info.mExtAddress) == router1.Get<Mac::Mac>().GetExtAddress());
VerifyOrQuit(!info.mIsChild);
VerifyMatchesIteration(fed, info);
Log("---------------------------------------------------------------------------------------");
Log("Leader: router that has left is no longer found");
router1Rloc16 = router1.Get<Mle::Mle>().GetRloc16();
router1.Get<Mle::Mle>().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;
}
+16
View File
@@ -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"