From e2d07be2350ed85ff23458ad38ec24c2d2cb69d9 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Tue, 14 Apr 2026 21:26:09 -0700 Subject: [PATCH] [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. --- tests/nexus/openthread-core-nexus-config.h | 1 + tests/nexus/platform/nexus_core.cpp | 235 +++++++++++++++++++++ tests/nexus/platform/nexus_core.hpp | 23 +- tests/nexus/platform/nexus_node.cpp | 5 +- tests/nexus/platform/nexus_node.hpp | 22 +- tests/nexus/platform/nexus_observer.hpp | 97 +++++++++ 6 files changed, 372 insertions(+), 11 deletions(-) create mode 100644 tests/nexus/platform/nexus_observer.hpp diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 87929a4d0..9231c26b6 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -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 diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index f6f25f244..7686df2ae 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -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().Down(); + node->Get().Stop(); + } + else + { + node->Get().Up(); + SuccessOrQuit(node->Get().Start()); + } + } +} + Node &Core::CreateNode(void) { Node *node; @@ -373,9 +398,174 @@ Node &Core::CreateNode(void) node->Get().SetReceiveCallback(Node::HandleIp6Receive, node); + if (mObserver) + { + node->Get().RegisterCallback(&Core::HandleNeighborTableChanged); + SuccessOrQuit(node->Get().RegisterCallback(&Core::HandleStateChanged, node)); + mObserver->OnNodeStateChanged(node); + } + return *node; } +void Core::HandleNeighborTableChanged(otNeighborTableEvent aEvent, const otNeighborTableEntryInfo *aInfo) +{ + NeighborTable::Event event = static_cast(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(aInfo->mInstance); + Neighbor *neighbor = + instance.Get().FindNeighbor(*extAddr, Neighbor::kInStateAnyExceptInvalid); + + if (neighbor != nullptr && !instance.Get().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(aContext); + + VerifyOrExit(observer != nullptr && node != nullptr); + + observer->OnNodeStateChanged(node); + + // Decoupled from flags to capture SED parent changes + switch (node->Get().GetRole()) + { + case Mle::kRoleChild: + { + Router::Info parentInfo; + + if (node->Get().GetParentInfo(parentInfo) == kErrorNone) + { + uint32_t srcId = node->GetInstance().GetId(); + uint32_t dstId = 0xffff; + Node *rxNode = + Core::Get().mNodes.FindMatching(static_cast(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().FindNeighbor(rxNode.Get().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::kMax; + if (mObserver) + { + mObserver->OnClearEvents(); + } +} + void Core::AdvanceTime(uint32_t aDuration) { uint64_t targetTime = mNow + (static_cast(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) diff --git a/tests/nexus/platform/nexus_core.hpp b/tests/nexus/platform/nexus_core.hpp index 224689f45..6106380ee 100644 --- a/tests/nexus/platform/nexus_core.hpp +++ b/tests/nexus/platform/nexus_core.hpp @@ -29,16 +29,18 @@ #ifndef OT_NEXUS_PLATFORM_NEXUS_CORE_HPP_ #define OT_NEXUS_PLATFORM_NEXUS_CORE_HPP_ +#include + +#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 &GetNodes(void) { return mNodes; } @@ -59,6 +64,9 @@ public: TimeMicro GetNowMicro(void) { return TimeMicro(static_cast(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 diff --git a/tests/nexus/platform/nexus_node.cpp b/tests/nexus/platform/nexus_node.cpp index 6d95435f2..bd18a1e33 100644 --- a/tests/nexus/platform/nexus_node.cpp +++ b/tests/nexus/platform/nexus_node.cpp @@ -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().SendEchoRequest(*message, messageInfo, aIdentifier)); } diff --git a/tests/nexus/platform/nexus_node.hpp b/tests/nexus/platform/nexus_node.hpp index 525127ec8..9e99f5c2e 100644 --- a/tests/nexus/platform/nexus_node.hpp +++ b/tests/nexus/platform/nexus_node.hpp @@ -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().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 Type &Get(void) { return Instance::Get(); } template const Type &Get(void) const { return AsConst(AsNonConst(this)->Get()); } - 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(*aInstance); } + static Node &From(otInstance *aInstance) + { + Instance *instance = static_cast(aInstance); + return *static_cast(instance); + } static void HandleIp6Receive(otMessage *aMessage, void *aContext); @@ -177,6 +189,7 @@ private: : Platform(static_cast(*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); } diff --git a/tests/nexus/platform/nexus_observer.hpp b/tests/nexus/platform/nexus_observer.hpp new file mode 100644 index 000000000..b8c61dbcd --- /dev/null +++ b/tests/nexus/platform/nexus_observer.hpp @@ -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 + +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_