mirror of
https://github.com/espressif/openthread.git
synced 2026-06-05 21:14:49 +00:00
[nexus] add WebAssembly support using Emscripten (#12904)
This commit adds support for building the Nexus simulator for WebAssembly (WASM) using the Emscripten toolchain. This enables the simulator to run in a web browser environment with a JavaScript-based control interface and visualization. Key implementation details: - Introduced `nexus_wasm.cpp` which defines Emscripten bindings (using Embind) for core simulation controls, including stepping time, node creation, topology orchestration, and state manipulation. - Implemented a `WasmObserver` and a global event queue to capture simulation events (node state changes, link updates, packet events) and expose them to JavaScript via a polling mechanism (`pollEvent`). - Updated the CMake build system to support the `EMSCRIPTEN` platform, configuring specific linker options for ES6 module export, modularization, and memory growth. - Enhanced `build.sh` to allow targeting WASM via `emcmake`. - Guarded file-system-dependent operations in `nexus_pcap.cpp` and adjusted `nexus_core.cpp` to handle WASM-specific constraints where standard I/O or multiple observers might not be applicable. - Added `test_wasm_bindings.mjs`, a Node.js-based smoke test that verifies the integrity of the WASM bindings and event pipeline. - Integrated `nexus-wasm-tests` into the GitHub Actions workflow to ensure continuous verification of the WASM build and functionality.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<uint8_t>((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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
+25
-6
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -63,6 +63,7 @@ public:
|
||||
|
||||
Node &CreateNode(void);
|
||||
Node *FindNodeById(uint32_t aNodeId);
|
||||
Node *FindNodeByExtAddress(const Mac::ExtAddress &aExtAddress);
|
||||
|
||||
LinkedList<Node> &GetNodes(void) { return mNodes; }
|
||||
|
||||
|
||||
@@ -33,6 +33,43 @@
|
||||
namespace ot {
|
||||
namespace Nexus {
|
||||
|
||||
const char *Node::GetExtendedRoleString(void) const
|
||||
{
|
||||
const char *roleStr;
|
||||
Mle::DeviceRole role = Get<Mle::Mle>().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<Mle::Mle>().IsFullThreadDevice())
|
||||
{
|
||||
roleStr = Get<Mle::Mle>().IsRouterRoleAllowed() ? "REED" : "FED";
|
||||
}
|
||||
else
|
||||
{
|
||||
roleStr = Get<Mle::Mle>().IsRxOnWhenIdle() ? "MED" : "SED";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
roleStr = "Unknown";
|
||||
break;
|
||||
}
|
||||
|
||||
return roleStr;
|
||||
}
|
||||
|
||||
void Node::Reset(void)
|
||||
{
|
||||
Instance *instance = &GetInstance();
|
||||
|
||||
@@ -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<Instance *>(aInstance);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -102,7 +102,9 @@ private:
|
||||
static constexpr uint16_t kTapChannelLength = 3;
|
||||
static constexpr uint8_t kTapChannelPage = 0;
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
FILE *mFile;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Nexus
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
|
||||
#include <openthread/thread.h>
|
||||
#include <openthread/thread_ftd.h>
|
||||
|
||||
#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<Mle::Mle>().GetRloc16());
|
||||
data.set("extAddress", aNode->Get<Mac::Mac>().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<val>("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<Event> 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_<Node::JoinMode>("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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -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 <path-to-nexus_live_demo.js>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
runTest(modulePath).catch(err => {
|
||||
console.error("Test failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user