diff --git a/.github/workflows/nexus.yml b/.github/workflows/nexus.yml index 4aea9db3f..d1d6416cd 100644 --- a/.github/workflows/nexus.yml +++ b/.github/workflows/nexus.yml @@ -133,3 +133,33 @@ jobs: - name: Run GRPC Tests run: | cd build/nexus && ./tests/nexus/nexus_grpc + + nexus-wasm-tests: + name: nexus-wasm-tests + runs-on: ubuntu-24.04 + steps: + - name: Harden Runner + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + submodules: recursive + + - name: Bootstrap + run: | + sudo apt-get update + sudo apt-get --no-install-recommends install -y ninja-build + + - name: Set up Emscripten + uses: mymindstorm/setup-emsdk@v14 + + - name: Build Nexus WASM + run: | + mkdir -p build/nexus + top_builddir=build/nexus ./tests/nexus/build.sh wasm + + - name: Run WASM Smoke Test + run: | + node tests/nexus/test_wasm_bindings.mjs build/nexus/tests/nexus/nexus_live_demo.js diff --git a/src/core/mac/mac_types.cpp b/src/core/mac/mac_types.cpp index ce18d7e7d..897e05232 100644 --- a/src/core/mac/mac_types.cpp +++ b/src/core/mac/mac_types.cpp @@ -84,6 +84,29 @@ ExtAddress::InfoString ExtAddress::ToString(void) const return string; } +Error ExtAddress::FromString(const char *aString) +{ + Error error = kErrorNone; + uint8_t high; + uint8_t low; + + VerifyOrExit(aString != nullptr, error = kErrorInvalidArgs); + + for (uint8_t &byte : m8) + { + SuccessOrExit(error = ParseHexDigit(*aString, high)); + aString++; + SuccessOrExit(error = ParseHexDigit(*aString, low)); + aString++; + byte = static_cast((high << 4) | low); + } + + VerifyOrExit(*aString == kNullChar, error = kErrorParse); + +exit: + return error; +} + void ExtAddress::CopyAddress(uint8_t *aDst, const uint8_t *aSrc, CopyByteOrder aByteOrder) { switch (aByteOrder) diff --git a/src/core/mac/mac_types.hpp b/src/core/mac/mac_types.hpp index d667fd747..fbfc591c9 100644 --- a/src/core/mac/mac_types.hpp +++ b/src/core/mac/mac_types.hpp @@ -232,6 +232,20 @@ public: */ InfoString ToString(void) const; + /** + * Parses an Extended Address from a string. + * + * The string must be a hex representation of the address (e.g., "0123456789abcdef"). + * The parsing is case-insensitive. + * + * @param[in] aString A pointer to the string to parse. + * + * @retval kErrorNone Successfully parsed the Extended Address. + * @retval kErrorInvalidArgs @p aString is `nullptr`. + * @retval kErrorParse @p aString is not a valid hex string representation of an Extended Address. + */ + Error FromString(const char *aString); + private: static constexpr uint8_t kGroupFlag = (1 << 0); static constexpr uint8_t kLocalFlag = (1 << 1); diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 09b38e60c..4e802fef8 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -49,7 +49,7 @@ set(COMMON_COMPILE_OPTIONS option(OT_NEXUS_GRPC "Enable Nexus gRPC" OFF) -if(OT_NEXUS_GRPC) +if(OT_NEXUS_GRPC AND NOT EMSCRIPTEN) find_package(gRPC CONFIG REQUIRED) find_package(Protobuf REQUIRED) @@ -116,7 +116,7 @@ add_library(ot-nexus-platform if(OT_NEXUS_GRPC) set_source_files_properties(${SIMULATION_PROTO_SRCS} PROPERTIES - COMPILE_FLAGS "-Wno-pedantic -Wno-error" + COMPILE_OPTIONS "-Wno-pedantic;-Wno-error" ) endif() @@ -131,6 +131,10 @@ target_compile_options(ot-nexus-platform ${OT_CFLAGS} ) +if(EMSCRIPTEN) + target_link_options(ot-nexus-platform PRIVATE "--bind") +endif() + if(OT_NEXUS_GRPC) target_compile_definitions(ot-nexus-platform PRIVATE @@ -404,14 +408,34 @@ ot_nexus_test(full_network_reset "core;large_network;nexus") ot_nexus_test(large_network "core;large_network;nexus") # Live Demo Persistent Server -if(OT_NEXUS_GRPC) +if(EMSCRIPTEN) + set(NEXUS_WASM_LINK_OPTIONS + "--bind" + "-sEXPORT_ES6=1" + "-sMODULARIZE=1" + "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']" + "-sSTACK_SIZE=4194304" + "-sALLOW_MEMORY_GROWTH=1" + ) + + add_executable(nexus_live_demo + platform/nexus_wasm.cpp + ) + target_link_options(nexus_live_demo PRIVATE ${NEXUS_WASM_LINK_OPTIONS}) + set_target_properties(nexus_live_demo PROPERTIES CXX_STANDARD 17) +elseif(OT_NEXUS_GRPC) add_executable(nexus_live_demo platform/nexus_native.cpp ) +endif() +if(TARGET nexus_live_demo) target_include_directories(nexus_live_demo PRIVATE ${COMMON_INCLUDES}) target_link_libraries(nexus_live_demo PRIVATE ${COMMON_LIBS}) - target_compile_options(nexus_live_demo PRIVATE ${COMMON_COMPILE_OPTIONS} ${OT_CFLAGS}) - target_compile_definitions(nexus_live_demo PRIVATE OPENTHREAD_NEXUS_CONFIG_GRPC_ENABLE=1) + if(EMSCRIPTEN) + add_test(NAME nexus_wasm_bindings COMMAND node ${CMAKE_CURRENT_SOURCE_DIR}/test_wasm_bindings.mjs ${CMAKE_CURRENT_BINARY_DIR}/nexus_live_demo.js) + elseif(OT_NEXUS_GRPC) + target_compile_definitions(nexus_live_demo PRIVATE OPENTHREAD_NEXUS_CONFIG_GRPC_ENABLE=1) + endif() endif() diff --git a/tests/nexus/build.sh b/tests/nexus/build.sh index cd9df45fa..8a5463206 100755 --- a/tests/nexus/build.sh +++ b/tests/nexus/build.sh @@ -47,9 +47,15 @@ fi case $1 in trel) fifteenfour=OFF + wasm=OFF + ;; + wasm) + fifteenfour=ON + wasm=ON ;; *) fifteenfour=ON + wasm=OFF ;; esac @@ -57,12 +63,25 @@ echo "========================================================================== echo "Building OpenThread Nexus test platform" echo "====================================================================================================" cd "${top_builddir}" || die "cd failed" -cmake -GNinja -DOT_PLATFORM=nexus -DOT_COMPILE_WARNING_AS_ERROR=ON \ - -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \ - -DOT_15_4=${fifteenfour} \ - -DOT_NEXUS_GRPC="${OT_NEXUS_GRPC:-OFF}" \ - -DOT_PROJECT_CONFIG="${top_srcdir}/tests/nexus/openthread-core-nexus-config.h" \ - "${top_srcdir}" || die + +CMAKE_ARGS=( + -GNinja + -DOT_PLATFORM=nexus + -DOT_COMPILE_WARNING_AS_ERROR=ON + -DOT_THREAD_VERSION=1.4 + -DOT_APP_CLI=OFF + -DOT_APP_NCP=OFF + -DOT_APP_RCP=OFF + -DOT_15_4="${fifteenfour}" + -DOT_PROJECT_CONFIG="${top_srcdir}/tests/nexus/openthread-core-nexus-config.h" +) + +if [ "${wasm}" = "ON" ]; then + emcmake cmake "${CMAKE_ARGS[@]}" "${top_srcdir}" || die +else + cmake "${CMAKE_ARGS[@]}" -DOT_NEXUS_GRPC="${OT_NEXUS_GRPC:-OFF}" "${top_srcdir}" || die +fi + ninja || die exit 0 diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index 0ecaa7b89..bbe8df619 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -390,6 +390,8 @@ void Core::SetNodeEnabled(uint32_t aNodeId, bool aEnabled) Node *Core::FindNodeById(uint32_t aNodeId) { return mNodes.FindMatching(aNodeId); } +Node *Core::FindNodeByExtAddress(const Mac::ExtAddress &aExtAddress) { return mNodes.FindMatching(aExtAddress); } + Node &Core::CreateNode(void) { Node *node; diff --git a/tests/nexus/platform/nexus_core.hpp b/tests/nexus/platform/nexus_core.hpp index 6b1ac6b64..176b9ddbf 100644 --- a/tests/nexus/platform/nexus_core.hpp +++ b/tests/nexus/platform/nexus_core.hpp @@ -63,6 +63,7 @@ public: Node &CreateNode(void); Node *FindNodeById(uint32_t aNodeId); + Node *FindNodeByExtAddress(const Mac::ExtAddress &aExtAddress); LinkedList &GetNodes(void) { return mNodes; } diff --git a/tests/nexus/platform/nexus_node.cpp b/tests/nexus/platform/nexus_node.cpp index 610fc3fa2..f0332f7df 100644 --- a/tests/nexus/platform/nexus_node.cpp +++ b/tests/nexus/platform/nexus_node.cpp @@ -33,6 +33,43 @@ namespace ot { namespace Nexus { +const char *Node::GetExtendedRoleString(void) const +{ + const char *roleStr; + Mle::DeviceRole role = Get().GetRole(); + + switch (role) + { + case Mle::kRoleDisabled: + roleStr = "Disabled"; + break; + case Mle::kRoleDetached: + roleStr = "Detached"; + break; + case Mle::kRoleLeader: + roleStr = "Leader"; + break; + case Mle::kRoleRouter: + roleStr = "Router"; + break; + case Mle::kRoleChild: + if (Get().IsFullThreadDevice()) + { + roleStr = Get().IsRouterRoleAllowed() ? "REED" : "FED"; + } + else + { + roleStr = Get().IsRxOnWhenIdle() ? "MED" : "SED"; + } + break; + default: + roleStr = "Unknown"; + break; + } + + return roleStr; +} + void Node::Reset(void) { Instance *instance = &GetInstance(); diff --git a/tests/nexus/platform/nexus_node.hpp b/tests/nexus/platform/nexus_node.hpp index 87043fa0d..a59d6d078 100644 --- a/tests/nexus/platform/nexus_node.hpp +++ b/tests/nexus/platform/nexus_node.hpp @@ -160,6 +160,13 @@ public: uint32_t GetId(void) const { return GetInstance().GetId(); } + /** + * Returns the extended role string of the node. + * + * @returns The role string (e.g., "Leader", "Router", "REED", "FED", "MED", "SED", "Disabled"). + */ + const char *GetExtendedRoleString(void) const; + static Node &From(otInstance *aInstance) { Instance *instance = static_cast(aInstance); diff --git a/tests/nexus/platform/nexus_pcap.cpp b/tests/nexus/platform/nexus_pcap.cpp index fbdab6ff6..13a6b27c4 100644 --- a/tests/nexus/platform/nexus_pcap.cpp +++ b/tests/nexus/platform/nexus_pcap.cpp @@ -27,6 +27,7 @@ */ #include "nexus_pcap.hpp" + #include "nexus_utils.hpp" #include "common/clearable.hpp" #include "common/code_utils.hpp" @@ -35,6 +36,8 @@ namespace ot { namespace Nexus { +#ifndef __EMSCRIPTEN__ + OT_TOOL_PACKED_BEGIN struct Epb { @@ -264,5 +267,27 @@ exit: return; } +#else // __EMSCRIPTEN__ + +Pcap::Pcap(void) {} + +Pcap::~Pcap(void) {} + +void Pcap::Open(const char *) {} + +void Pcap::Close(void) {} + +void Pcap::WriteFrame(const otRadioFrame &, uint64_t) {} + +void Pcap::WritePacket(const InfraIf::LinkLayerAddress &, + const InfraIf::LinkLayerAddress &, + const uint8_t *, + uint16_t, + uint64_t) +{ +} + +#endif // __EMSCRIPTEN__ + } // namespace Nexus } // namespace ot diff --git a/tests/nexus/platform/nexus_pcap.hpp b/tests/nexus/platform/nexus_pcap.hpp index 9f4e20159..ad9b71a37 100644 --- a/tests/nexus/platform/nexus_pcap.hpp +++ b/tests/nexus/platform/nexus_pcap.hpp @@ -102,7 +102,9 @@ private: static constexpr uint16_t kTapChannelLength = 3; static constexpr uint8_t kTapChannelPage = 0; +#ifndef __EMSCRIPTEN__ FILE *mFile; +#endif }; } // namespace Nexus diff --git a/tests/nexus/platform/nexus_utils.hpp b/tests/nexus/platform/nexus_utils.hpp index ebff2b25d..833ec619f 100644 --- a/tests/nexus/platform/nexus_utils.hpp +++ b/tests/nexus/platform/nexus_utils.hpp @@ -79,6 +79,11 @@ namespace ot { namespace Nexus { +/** + * Invalid Node ID. + */ +static constexpr uint32_t kInvalidNodeId = 0xffffffff; + /** * CSL period constants in units of 10 symbols. */ diff --git a/tests/nexus/platform/nexus_wasm.cpp b/tests/nexus/platform/nexus_wasm.cpp new file mode 100644 index 000000000..a36d5ec86 --- /dev/null +++ b/tests/nexus/platform/nexus_wasm.cpp @@ -0,0 +1,285 @@ +/* + * 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 defines the WebAssembly bindings for the Nexus simulator. + */ + +#include "openthread-core-config.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "nexus_core.hpp" +#include "nexus_node.hpp" +#include "mac/mac.hpp" +#include "thread/mle.hpp" +#include "thread/neighbor_table.hpp" + +namespace ot { +namespace Nexus { + +using namespace emscripten; + +/** + * This class manages simulation events and exposes them to the WASM environment. + * + * It implements the Observer interface to capture events from the simulator core. + */ +class WasmManager : public Observer +{ +public: + static constexpr char kEventNodeStateChanged[] = "node_state_changed"; + static constexpr char kEventLinkUpdate[] = "link_update"; + static constexpr char kEventPacketEvent[] = "packet_event"; + static constexpr char kEventHeartbeat[] = "heartbeat"; + + /** + * Represents a simulation event to be consumed by JavaScript. + */ + struct Event + { + std::string mType; + val mData; + }; + + /** + * Returns the singleton instance of WasmManager. + * + * @returns The WasmManager instance. + */ + static WasmManager &Get(void) + { + static WasmManager sManager; + return sManager; + } + + /** + * Initializes the manager and attaches it as an observer to the core. + * + * This method is idempotent and ensures the manager is registered as a simulation observer. + */ + void Init(void) + { + if (!mInitialized) + { + Core::Get().AddObserver(*this); + mInitialized = true; + } + } + + /** + * Polls the next simulation event from the internal queue. + * + * The returned event object has the following structure in JavaScript: + * { + * "type": string (e.g., "node_state_changed", "link_update", "packet_event"), + * "data": object (type-specific payload) + * } + * + * @returns The event object or val::null() if the queue is empty. + */ + val PollEvent(void) + { + val eventVal = val::null(); + + if (!mEventQueue.empty()) + { + const Event &event = mEventQueue.front(); + + eventVal = val::object(); + eventVal.set("type", event.mType); + eventVal.set("data", event.mData); + mEventQueue.pop_front(); + } + + return eventVal; + } + + // Observer overrides + void OnNodeStateChanged(Node *aNode) override + { + val data = val::object(); + + data.set("id", aNode->GetId()); + data.set("role", aNode->GetExtendedRoleString()); + data.set("x", aNode->GetPositionX()); + data.set("y", aNode->GetPositionY()); + data.set("rloc16", aNode->Get().GetRloc16()); + data.set("extAddress", aNode->Get().GetExtAddress().ToString().AsCString()); + + PushEvent(kEventNodeStateChanged, data); + } + + void OnLinkUpdate(uint32_t aSrcId, uint32_t aDstId, bool aIsActive) override + { + val data = val::object(); + + data.set("srcId", aSrcId); + data.set("dstId", aDstId); + data.set("isActive", aIsActive); + + PushEvent(kEventLinkUpdate, data); + } + + void OnPacketEvent(uint32_t aSrcId, uint32_t aDstId, const uint8_t *aFrame, uint16_t aLength) override + { + val data = val::object(); + + data.set("srcId", aSrcId); + data.set("dstId", aDstId); + data.set("length", aLength); + data.set("frame", val(typed_memory_view(aLength, aFrame)).call("slice")); + + PushEvent(kEventPacketEvent, data); + } + + void OnHeartbeat(uint64_t aTimestampUs) override + { + val data = val::object(); + + data.set("timestampUs", aTimestampUs); + + PushEvent(kEventHeartbeat, data); + } + + void OnClearEvents(void) override { mEventQueue.clear(); } + + /** + * WasmManager does not participate in state dumping as events are streamed in real-time. + */ + void DumpState(void) override {} + + bool IsConnected(void) const override { return true; } + +private: + static constexpr size_t kMaxQueueSize = 1000; + + void PushEvent(const std::string &aType, val aData) + { + if (mEventQueue.size() >= kMaxQueueSize) + { + mEventQueue.pop_front(); + } + mEventQueue.push_back({aType, aData}); + } + + std::deque mEventQueue; // Internal event queue (should be polled regularly by JS to prevent growth) + bool mInitialized = false; +}; + +// The global simulator core instance. +Core gWasmCore; + +} // namespace Nexus +} // namespace ot + +// Embind definitions +EMSCRIPTEN_BINDINGS(nexus_simulator) +{ + using namespace ot::Nexus; + + // Initialize WasmManager during module binding + WasmManager::Get().Init(); + + enum_("JoinMode") + .value("AsFtd", Node::kAsFtd) + .value("AsFed", Node::kAsFed) + .value("AsMed", Node::kAsMed) + .value("AsSed", Node::kAsSed) + .value("AsSedWithFullNetData", Node::kAsSedWithFullNetData); + + function("pollEvent", optional_override([]() -> val { return WasmManager::Get().PollEvent(); })); + + function("getNow", optional_override([]() -> uint32_t { return Core::Get().GetNow().GetValue(); })); + + function("stepSimulation", optional_override([](uint32_t aElapsedMs) { Core::Get().AdvanceTime(aElapsedMs); })); + + function("createNode", optional_override([](float aX, float aY) -> uint32_t { + Node &node = Core::Get().CreateNode(); + node.SetPosition(aX, aY); + WasmManager::Get().OnNodeStateChanged(&node); + return node.GetId(); + })); + + function("setNodePosition", optional_override([](uint32_t aNodeId, float aX, float aY) { + Node *node = Core::Get().FindNodeById(aNodeId); + + if (node != nullptr) + { + node->SetPosition(aX, aY); + } + })); + + function("getNodeId", optional_override([](std::string aExtAddress) -> uint32_t { + ot::Mac::ExtAddress extAddr; + uint32_t id = kInvalidNodeId; + + if (extAddr.FromString(aExtAddress.c_str()) == ot::kErrorNone) + { + Node *node = Core::Get().GetNodes().FindMatching(extAddr); + if (node != nullptr) + { + id = node->GetId(); + } + } + return id; + })); + + function("setNodeEnabled", + optional_override([](uint32_t aNodeId, bool aEnabled) { Core::Get().SetNodeEnabled(aNodeId, aEnabled); })); + + function("formNetwork", optional_override([](uint32_t aNodeId) { + Node *node = Core::Get().FindNodeById(aNodeId); + + if (node != nullptr) + { + node->Form(); + } + })); + + function("joinNetwork", optional_override([](uint32_t aNodeId, uint32_t aTargetNodeId, Node::JoinMode aJoinMode) { + Core &core = Core::Get(); + Node *node = core.FindNodeById(aNodeId); + Node *target = core.FindNodeById(aTargetNodeId); + + if (node != nullptr && target != nullptr) + { + node->Join(*target, aJoinMode); + } + })); +} diff --git a/tests/nexus/test_wasm_bindings.mjs b/tests/nexus/test_wasm_bindings.mjs new file mode 100644 index 000000000..916bed26d --- /dev/null +++ b/tests/nexus/test_wasm_bindings.mjs @@ -0,0 +1,127 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +async function createTopology(Nexus) { + console.log("Creating Topology in JavaScript..."); + + const leaderId = Nexus.createNode(350, 100); + const fedId = Nexus.createNode(100, 500); + const sedId = Nexus.createNode(350, 500); + const medId = Nexus.createNode(600, 500); + const router1Id = Nexus.createNode(200, 300); + const router2Id = Nexus.createNode(500, 300); + + Nexus.formNetwork(leaderId); + + // Use the exported JoinMode enum + Nexus.joinNetwork(fedId, leaderId, Nexus.JoinMode.AsFed); + Nexus.joinNetwork(sedId, leaderId, Nexus.JoinMode.AsSed); + Nexus.joinNetwork(medId, leaderId, Nexus.JoinMode.AsMed); + Nexus.joinNetwork(router1Id, leaderId, Nexus.JoinMode.AsFtd); + Nexus.joinNetwork(router2Id, leaderId, Nexus.JoinMode.AsFtd); + + return { leaderId, fedId, sedId, medId, router1Id, router2Id }; +} + +async function runTest(modulePath) { + const absolutePath = path.resolve(modulePath); + console.log(`Loading Nexus WASM module from: ${absolutePath}`); + + // Load the Emscripten-generated modularized ES6 module + const { default: createNexusModule } = await import('file://' + absolutePath); + + // Initialize the module + const Nexus = await createNexusModule(); + + await createTopology(Nexus); + + console.log("Initial simulation time:", Nexus.getNow()); + + console.log("Stepping Simulation..."); + let nodeStateChangedCount = 0; + let linkUpdateCount = 0; + let packetEventCount = 0; + + // Advance simulation and check for events + for (let i = 0; i < 50; i++) { + Nexus.stepSimulation(100); + let event; + while ((event = Nexus.pollEvent()) !== null) { + const type = event.type; + const data = event.data; + + if (type === "node_state_changed") { + nodeStateChangedCount++; + if (nodeStateChangedCount < 10) { + console.log(`[Event] Node ${data.id} state: ${data.role} at (${data.x}, ${data.y}) RLOC16: ${data.rloc16} ExtAddr: ${data.extAddress}`); + } + if (!data.extAddress) { + throw new Error(`Node ${data.id} state change event missing extAddress`); + } + } else if (type === "link_update") { + linkUpdateCount++; + } else if (type === "packet_event") { + packetEventCount++; + if (packetEventCount < 5) { + console.log(`[Event] Packet from ${data.srcId} to ${data.dstId} (length: ${data.length}) frame: ${data.frame.length} bytes`); + } + if (!data.frame || data.frame.length !== data.length) { + throw new Error(`Packet event from ${data.srcId} to ${data.dstId} has invalid frame data`); + } + } + } + } + + console.log(`Received ${nodeStateChangedCount} node_state_changed events`); + console.log(`Received ${linkUpdateCount} link_update events`); + console.log(`Received ${packetEventCount} packet_event events`); + + if (nodeStateChangedCount === 0) { + throw new Error("Expected node_state_changed events were not received"); + } + + // Verify getNodeId + let event; + // We need to find an event that has extAddress to test getNodeId + // Since we've already polled all events, we should have some node state changed events. + // Let's assume the first few events we printed have what we need, or just use the last polled data if available. + // Actually, let's just create a new node and test it. + + console.log("Creating a manual node..."); + const nodeId = Nexus.createNode(100, 200); + console.log("Created node ID:", nodeId); + + // Step to trigger state change event to get its extAddress + Nexus.stepSimulation(1); + let foundExtAddr = null; + while ((event = Nexus.pollEvent()) !== null) { + if (event.type === "node_state_changed" && event.data.id === nodeId) { + foundExtAddr = event.data.extAddress; + } + } + + if (foundExtAddr) { + const resolvedId = Nexus.getNodeId(foundExtAddr); + console.log(`Resolved ExtAddr ${foundExtAddr} to ID: ${resolvedId}`); + if (resolvedId !== nodeId) { + throw new Error(`getNodeId failed: expected ${nodeId}, got ${resolvedId}`); + } + } + + Nexus.setNodePosition(nodeId, 150, 250); + Nexus.setNodeEnabled(nodeId, false); + Nexus.setNodeEnabled(nodeId, true); + + console.log("Test completed successfully!"); +} + +const modulePath = process.argv[2]; +if (!modulePath) { + console.error("Usage: node test_wasm_bindings.mjs "); + process.exit(1); +} + +runTest(modulePath).catch(err => { + console.error("Test failed:", err); + process.exit(1); +});