Files
openthread/tests/nexus/test_wasm_bindings.mjs
Jonathan Hui 27321a2110 [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.
2026-04-17 15:25:28 -05:00

128 lines
4.7 KiB
JavaScript

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);
});