[nexus] introduce simulation observer interface and hooks (#12894)

This commit introduces the `SimulationObserver` interface and integrates
it into the Nexus core simulation logic. This allows external systems to
observe node state changes, link updates, and packet events in real-time.

Key changes:
- Defined `SimulationObserver` interface to handle node state changes,
  link updates, packet events, and event clearing.
- Added `SetObserver` and `GetObserver` methods to the `Core` class.
- Implemented `Core::HandleNeighborTableChanged` to notify the observer
  of neighbor additions and removals.
- Implemented `Core::HandleStateChanged` to track node role transitions
  and parent changes, updating links accordingly.
- Integrated packet event notification in `Core::ProcessRadio`,
  including basic destination node ID resolution for unicast frames.
- Added `Core::SetNodeEnabled` to allow enabling or disabling Thread and
  MLE on specific nodes at runtime.
- Updated `Core::Reset` to clear events via the observer.
- Increased `OPENTHREAD_CONFIG_MAX_STATECHANGE_HANDLERS` to accommodate
  the new nexus state change handler.
- Added `mLastParentId` to `Node` class to correctly manage link updates
  during parent switches or detachment.
This commit is contained in:
Jonathan Hui
2026-04-14 21:26:09 -07:00
committed by GitHub
parent 7829782b06
commit e2d07be235
6 changed files with 372 additions and 11 deletions
@@ -151,6 +151,7 @@
#define OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE 1
#define OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE 1
#define OPENTHREAD_CONFIG_TMF_SNOOP_CACHE_ENTRY_TIMEOUT 3
#define OPENTHREAD_CONFIG_MAX_STATECHANGE_HANDLERS 4
#define OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE 1
#define OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE 1
#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 1
+235
View File
@@ -34,6 +34,8 @@
#include "mac_frame.h"
#include "nexus_node.hpp"
#include "nexus_radio_model.hpp"
#include "thread/child_table.hpp"
#include "thread/neighbor_table.hpp"
namespace ot {
namespace Nexus {
@@ -46,6 +48,8 @@ Core::Core(void)
, mPendingAction(false)
, mSaveNodeLogs(false)
, mNow(0)
, mObserver(nullptr)
{
const char *pcapFile;
const char *saveLogs;
@@ -346,6 +350,27 @@ Core::~Core(void)
sInUse = false;
}
void Core::SetNodeEnabled(uint32_t aNodeId, bool aEnabled)
{
Node *node = GetNodes().FindMatching(aNodeId);
if (node != nullptr)
{
Log("Core::SetNodeEnabled: Node %u found. Setting Thread enabled=%d", aNodeId, aEnabled);
if (!aEnabled)
{
node->Get<ThreadNetif>().Down();
node->Get<Mle::Mle>().Stop();
}
else
{
node->Get<ThreadNetif>().Up();
SuccessOrQuit(node->Get<Mle::Mle>().Start());
}
}
}
Node &Core::CreateNode(void)
{
Node *node;
@@ -373,9 +398,174 @@ Node &Core::CreateNode(void)
node->Get<Ip6::Ip6>().SetReceiveCallback(Node::HandleIp6Receive, node);
if (mObserver)
{
node->Get<NeighborTable>().RegisterCallback(&Core::HandleNeighborTableChanged);
SuccessOrQuit(node->Get<Notifier>().RegisterCallback(&Core::HandleStateChanged, node));
mObserver->OnNodeStateChanged(node);
}
return *node;
}
void Core::HandleNeighborTableChanged(otNeighborTableEvent aEvent, const otNeighborTableEntryInfo *aInfo)
{
NeighborTable::Event event = static_cast<NeighborTable::Event>(aEvent);
Core &core = Core::Get();
uint32_t srcId = 0;
uint32_t dstId = 0;
bool foundSrc = false;
bool foundDst = false;
bool isActive;
const Mac::ExtAddress *extAddr = nullptr;
VerifyOrExit(core.mObserver);
Log("HandleNeighborTableChanged: Event %d", event);
{
Node &srcNode = Node::From(aInfo->mInstance);
srcId = srcNode.GetId();
foundSrc = true;
}
switch (event)
{
case NeighborTable::kChildAdded:
case NeighborTable::kChildRemoved:
extAddr = &AsCoreType(&aInfo->mInfo.mChild.mExtAddress);
isActive = (event == NeighborTable::kChildAdded);
if (event == NeighborTable::kChildRemoved)
{
Instance &instance = *static_cast<Instance *>(aInfo->mInstance);
Neighbor *neighbor =
instance.Get<NeighborTable>().FindNeighbor(*extAddr, Neighbor::kInStateAnyExceptInvalid);
if (neighbor != nullptr && !instance.Get<ChildTable>().Contains(*neighbor))
{
Log("Suppressing CHILD_REMOVED event because node is in router table");
ExitNow();
}
}
break;
case NeighborTable::kRouterAdded:
case NeighborTable::kRouterRemoved:
extAddr = &AsCoreType(&aInfo->mInfo.mRouter.mExtAddress);
isActive = (event == NeighborTable::kRouterAdded);
break;
default:
break;
}
if (extAddr != nullptr)
{
Node *node = core.GetNodes().FindMatching(*extAddr);
if (node != nullptr)
{
dstId = node->GetInstance().GetId();
foundDst = true;
}
}
if (foundSrc && foundDst)
{
core.mObserver->OnLinkUpdate(srcId, dstId, isActive);
}
else
{
Log("HandleNeighborTableChanged: Failed to find srcId or dstId. foundSrc: %d, foundDst: %d", foundSrc,
foundDst);
}
exit:
return;
}
void Core::HandleStateChanged(otChangedFlags aFlags, void *aContext)
{
OT_UNUSED_VARIABLE(aFlags);
Observer *observer = Core::Get().GetObserver();
Node *node = static_cast<Node *>(aContext);
VerifyOrExit(observer != nullptr && node != nullptr);
observer->OnNodeStateChanged(node);
// Decoupled from flags to capture SED parent changes
switch (node->Get<Mle::Mle>().GetRole())
{
case Mle::kRoleChild:
{
Router::Info parentInfo;
if (node->Get<Mle::Mle>().GetParentInfo(parentInfo) == kErrorNone)
{
uint32_t srcId = node->GetInstance().GetId();
uint32_t dstId = 0xffff;
Node *rxNode =
Core::Get().mNodes.FindMatching(static_cast<const Mac::ExtAddress &>(parentInfo.mExtAddress));
if (rxNode != nullptr)
{
dstId = rxNode->GetInstance().GetId();
}
if (dstId != 0xffff && dstId != node->GetLastParentId())
{
if (node->GetLastParentId() != 0xffff)
{
observer->OnLinkUpdate(srcId, node->GetLastParentId(), false);
}
node->SetLastParentId(dstId);
observer->OnLinkUpdate(srcId, dstId, true);
}
}
break;
}
case Mle::kRoleDetached:
{
uint32_t srcId = node->GetInstance().GetId();
for (Node &rxNode : Core::Get().mNodes)
{
if (&rxNode == node)
{
continue;
}
if (node->Get<NeighborTable>().FindNeighbor(rxNode.Get<Mac::Mac>().GetExtAddress(),
Neighbor::kInStateValid) != nullptr)
{
uint32_t dstId = rxNode.GetInstance().GetId();
observer->OnLinkUpdate(srcId, dstId, false);
observer->OnLinkUpdate(dstId, srcId, false);
}
}
if (node->GetLastParentId() != 0xffff)
{
observer->OnLinkUpdate(srcId, node->GetLastParentId(), false);
node->SetLastParentId(0xffff);
}
break;
}
default:
break;
}
exit:
return;
}
void Core::UpdateNextAlarmMilli(const Alarm &aAlarm)
{
if (aAlarm.mScheduled)
@@ -414,6 +604,20 @@ void Core::UpdateNextAlarmMicro(const Alarm &aAlarm)
}
}
bool Core::IsUiConnected(void) const { return mObserver && mObserver->IsConnected(); }
void Core::Reset(void)
{
mNodes.Clear();
mCurNodeId = 0;
mNow = 0;
mNextAlarmTime = NumericLimits<uint64_t>::kMax;
if (mObserver)
{
mObserver->OnClearEvents();
}
}
void Core::AdvanceTime(uint32_t aDuration)
{
uint64_t targetTime = mNow + (static_cast<uint64_t>(aDuration) * 1000u);
@@ -487,6 +691,37 @@ void Core::ProcessRadio(Node &aNode)
mPcap.WriteFrame(aNode.mRadio.mTxFrame, mNow);
if (mObserver)
{
uint32_t dstNodeId = 0xffff; // Default to broadcast / unknown
if (!dstAddr.IsBroadcast())
{
for (Node &rxNode : mNodes)
{
if (&rxNode == &aNode)
{
continue;
}
if (rxNode.mRadio.Matches(dstAddr, dstPanId))
{
dstNodeId = rxNode.GetInstance().GetId();
break;
}
}
}
if (!dstAddr.IsBroadcast() && dstNodeId == 0xffff)
{
Log("ProcessRadio: Failed to resolve dstNodeId for unicast from node %u to %s", aNode.GetInstance().GetId(),
dstAddr.ToString().AsCString());
}
mObserver->OnPacketEvent(aNode.GetInstance().GetId(), dstNodeId, aNode.mRadio.mTxFrame.GetPsdu(),
aNode.mRadio.mTxFrame.GetLength());
}
otPlatRadioTxStarted(&aNode.GetInstance(), &aNode.mRadio.mTxFrame);
for (Node &rxNode : mNodes)
+18 -5
View File
@@ -29,16 +29,18 @@
#ifndef OT_NEXUS_PLATFORM_NEXUS_CORE_HPP_
#define OT_NEXUS_PLATFORM_NEXUS_CORE_HPP_
#include <stdio.h>
#include "nexus_alarm.hpp"
#include "nexus_observer.hpp"
#include "nexus_pcap.hpp"
#include "nexus_radio.hpp"
#include "nexus_utils.hpp"
#include "common/array.hpp"
#include "common/owning_list.hpp"
#include "instance/instance.hpp"
#include "thread/key_manager.hpp"
#include "nexus_alarm.hpp"
#include "nexus_pcap.hpp"
#include "nexus_radio.hpp"
#include "nexus_utils.hpp"
namespace ot {
namespace Nexus {
@@ -52,6 +54,9 @@ public:
static Core &Get(void) { return *sCore; }
void SetObserver(Observer *aObserver) { mObserver = aObserver; }
Observer *GetObserver(void) { return mObserver; }
Node &CreateNode(void);
LinkedList<Node> &GetNodes(void) { return mNodes; }
@@ -59,6 +64,9 @@ public:
TimeMicro GetNowMicro(void) { return TimeMicro(static_cast<uint32_t>(mNow)); }
uint64_t GetNowMicro64(void) const { return mNow; }
void AdvanceTime(uint32_t aDuration);
bool IsUiConnected(void) const;
void Reset(void);
void SetNodeEnabled(uint32_t aNodeId, bool aEnabled);
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Test specific helper methods
@@ -86,6 +94,9 @@ public:
Node *FindNodeByThreadAddress(const Ip6::Address &aAddress);
Node *FindNodeByInfraIfAddress(const Ip6::Address &aAddress);
static void HandleNeighborTableChanged(otNeighborTableEvent aEvent, const otNeighborTableEntryInfo *aInfo);
static void HandleStateChanged(otChangedFlags aFlags, void *aContext);
private:
static constexpr int8_t kDefaultRxRssi = -20;
static constexpr uint8_t kDefaultRxLqi = 255;
@@ -135,6 +146,8 @@ private:
bool mSaveNodeLogs;
uint64_t mNow;
uint64_t mNextAlarmTime;
Observer *mObserver;
};
} // namespace Nexus
+2 -3
View File
@@ -28,6 +28,8 @@
#include "nexus_node.hpp"
#include "nexus_utils.hpp"
namespace ot {
namespace Nexus {
@@ -133,9 +135,6 @@ void Node::SendEchoRequest(const Ip6::Address &aDestination,
messageInfo.SetSockAddr(*aSrcAddress);
}
Log("Sending Echo Request from Node %lu (%s) to %s (payload-size:%u)", ToUlong(GetId()), GetName(),
aDestination.ToString().AsCString(), aPayloadSize);
SuccessOrQuit(Get<Ip6::Icmp>().SendEchoRequest(*message, messageInfo, aIdentifier));
}
+19 -3
View File
@@ -132,6 +132,9 @@ public:
bool Matches(const Ip6::Address &aAddress, AddressNetif aNetif) const;
bool Matches(uint32_t aId) const { return GetInstance().GetId() == aId; }
bool Matches(const Mac::ExtAddress &aExtAddress) const { return Get<Mac::Mac>().GetExtAddress() == aExtAddress; }
void SetName(const char *aName) { mName.Clear().Append("%s", aName); }
void SetName(const char *aPrefix, uint16_t aIndex);
const char *GetName(void) const { return mName.AsCString(); }
@@ -143,16 +146,25 @@ public:
}
float GetPositionX(void) const { return mX; }
float GetPositionY(void) const { return mY; }
uint32_t GetLastParentId(void) const { return mLastParentId; }
void SetLastParentId(uint32_t aId) { mLastParentId = aId; }
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
template <typename Type> Type &Get(void) { return Instance::Get<Type>(); }
template <typename Type> const Type &Get(void) const { return AsConst(AsNonConst(this)->Get<Type>()); }
Instance &GetInstance(void) { return *this; }
Instance &GetInstance(void) { return *this; }
const Instance &GetInstance(void) const { return *this; }
uint32_t GetId(void) { return GetInstance().GetId(); }
uint32_t GetId(void) const { return GetInstance().GetId(); }
static Node &From(otInstance *aInstance) { return static_cast<Node &>(*aInstance); }
static Node &From(otInstance *aInstance)
{
Instance *instance = static_cast<Instance *>(aInstance);
return *static_cast<Node *>(instance);
}
static void HandleIp6Receive(otMessage *aMessage, void *aContext);
@@ -177,6 +189,7 @@ private:
: Platform(static_cast<Instance &>(*this))
, mX(0.0f)
, mY(0.0f)
, mLastParentId(0xffff)
{
}
@@ -185,6 +198,9 @@ private:
String<32> mName;
float mX;
float mY;
uint32_t mLastParentId;
public:
};
inline Node &AsNode(otInstance *aInstance) { return Node::From(aInstance); }
+97
View File
@@ -0,0 +1,97 @@
/*
* 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 Nexus simulation observer.
*/
#ifndef OT_NEXUS_PLATFORM_NEXUS_OBSERVER_HPP_
#define OT_NEXUS_PLATFORM_NEXUS_OBSERVER_HPP_
#include <stdint.h>
namespace ot {
namespace Nexus {
class Node;
/**
* Represents an observer for the Nexus simulation.
*
* This class defines the interface for receiving simulation events from the Nexus core.
*/
class Observer
{
public:
virtual ~Observer(void) = default;
/**
* This method is called when a node state has changed.
*
* @param[in] aNode A pointer to the node whose state has changed.
*/
virtual void OnNodeStateChanged(Node *aNode) = 0;
/**
* This method is called when a link state between two nodes has been updated.
*
* @param[in] aSrcId The source node ID.
* @param[in] aDstId The destination node ID.
* @param[in] aIsActive TRUE if the link is active, FALSE otherwise.
*/
virtual void OnLinkUpdate(uint32_t aSrcId, uint32_t aDstId, bool aIsActive) = 0;
/**
* This method is called when a packet event occurs.
*
* @param[in] aSenderId The ID of the node that sent the packet.
* @param[in] aDestinationId The ID of the destination node, or 0xffff for broadcasts or unresolved nodes.
* @param[in] aData A pointer to the packet data.
* @param[in] aLen The length of the packet data.
*/
virtual void OnPacketEvent(uint32_t aSenderId, uint32_t aDestinationId, const uint8_t *aData, uint16_t aLen) = 0;
/**
* This method is called to clear all events.
*/
virtual void OnClearEvents(void) = 0;
/**
* This method indicates whether or not the observer is connected.
*
* @retval TRUE If the observer is connected.
* @retval FALSE If the observer is not connected.
*/
virtual bool IsConnected(void) const = 0;
};
} // namespace Nexus
} // namespace ot
#endif // OT_NEXUS_PLATFORM_NEXUS_OBSERVER_HPP_