mirror of
https://github.com/espressif/openthread.git
synced 2026-10-07 16:27:35 +00:00
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.
294 lines
10 KiB
C++
294 lines
10 KiB
C++
/*
|
|
* 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.
|
|
*/
|
|
|
|
#include "nexus_pcap.hpp"
|
|
|
|
#include "nexus_utils.hpp"
|
|
#include "common/clearable.hpp"
|
|
#include "common/code_utils.hpp"
|
|
#include "common/encoding.hpp"
|
|
|
|
namespace ot {
|
|
namespace Nexus {
|
|
|
|
#ifndef __EMSCRIPTEN__
|
|
|
|
OT_TOOL_PACKED_BEGIN
|
|
struct Epb
|
|
{
|
|
uint32_t mBlockType;
|
|
uint32_t mBlockTotalLength;
|
|
uint32_t mInterfaceId;
|
|
uint32_t mTimestampHigh;
|
|
uint32_t mTimestampLow;
|
|
uint32_t mCapturedLen;
|
|
uint32_t mOriginalLen;
|
|
} OT_TOOL_PACKED_END;
|
|
|
|
Pcap::Pcap(void)
|
|
: mFile(nullptr)
|
|
{
|
|
}
|
|
|
|
Pcap::~Pcap(void) { Close(); }
|
|
|
|
void Pcap::Open(const char *aFilename)
|
|
{
|
|
OT_TOOL_PACKED_BEGIN
|
|
struct Shb
|
|
{
|
|
uint32_t mBlockType;
|
|
uint32_t mBlockTotalLength;
|
|
uint32_t mByteOrderMagic;
|
|
uint16_t mVersionMajor;
|
|
uint16_t mVersionMinor;
|
|
uint64_t mSectionLength;
|
|
uint32_t mBlockTotalLength2;
|
|
} OT_TOOL_PACKED_END shb;
|
|
|
|
OT_TOOL_PACKED_BEGIN
|
|
struct Idb
|
|
{
|
|
uint32_t mBlockType;
|
|
uint32_t mBlockTotalLength;
|
|
uint16_t mLinkType;
|
|
uint16_t mReserved;
|
|
uint32_t mSnapLen;
|
|
uint32_t mBlockTotalLength2;
|
|
} OT_TOOL_PACKED_END idb;
|
|
|
|
Close();
|
|
|
|
mFile = fopen(aFilename, "wb");
|
|
VerifyOrExit(mFile != nullptr);
|
|
|
|
ClearAllBytes(shb);
|
|
shb.mBlockType = LittleEndian::HostSwap(kPcapngShbType);
|
|
shb.mBlockTotalLength = LittleEndian::HostSwap(static_cast<uint32_t>(sizeof(shb)));
|
|
shb.mByteOrderMagic = LittleEndian::HostSwap(kPcapngByteOrderMagic);
|
|
shb.mVersionMajor = LittleEndian::HostSwap(kPcapngVersionMajor);
|
|
shb.mVersionMinor = LittleEndian::HostSwap(kPcapngVersionMinor);
|
|
shb.mSectionLength = 0xffffffffffffffffULL;
|
|
shb.mBlockTotalLength2 = shb.mBlockTotalLength;
|
|
VerifyOrExit(fwrite(&shb, sizeof(shb), 1, mFile) == 1, Close());
|
|
|
|
ClearAllBytes(idb);
|
|
idb.mBlockType = LittleEndian::HostSwap(kPcapngIdbType);
|
|
idb.mBlockTotalLength = LittleEndian::HostSwap(static_cast<uint32_t>(sizeof(idb)));
|
|
idb.mLinkType = LittleEndian::HostSwap(static_cast<uint16_t>(kPcapngLinkTypeIeee802154));
|
|
idb.mSnapLen = LittleEndian::HostSwap(kPcapngSnapLen);
|
|
idb.mBlockTotalLength2 = idb.mBlockTotalLength;
|
|
VerifyOrExit(fwrite(&idb, sizeof(idb), 1, mFile) == 1, Close());
|
|
|
|
ClearAllBytes(idb);
|
|
idb.mBlockType = LittleEndian::HostSwap(kPcapngIdbType);
|
|
idb.mBlockTotalLength = LittleEndian::HostSwap(static_cast<uint32_t>(sizeof(idb)));
|
|
idb.mLinkType = LittleEndian::HostSwap(static_cast<uint16_t>(kPcapngLinkTypeEthernet));
|
|
idb.mSnapLen = LittleEndian::HostSwap(kPcapngSnapLen);
|
|
idb.mBlockTotalLength2 = idb.mBlockTotalLength;
|
|
VerifyOrExit(fwrite(&idb, sizeof(idb), 1, mFile) == 1, Close());
|
|
|
|
VerifyOrExit(fflush(mFile) == 0, Close());
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Pcap::Close(void)
|
|
{
|
|
VerifyOrExit(mFile != nullptr);
|
|
|
|
fclose(mFile);
|
|
mFile = nullptr;
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Pcap::WriteFrame(const otRadioFrame &aFrame, uint64_t aTimeUs)
|
|
{
|
|
Epb epb;
|
|
|
|
OT_TOOL_PACKED_BEGIN
|
|
struct TapHeader
|
|
{
|
|
uint16_t mVersion;
|
|
uint16_t mLength;
|
|
} OT_TOOL_PACKED_END tapHeader;
|
|
|
|
uint32_t tapLength;
|
|
uint32_t packetLen;
|
|
uint32_t paddedLen;
|
|
uint32_t blockTotalLength;
|
|
uint32_t padding = 0;
|
|
|
|
VerifyOrExit(mFile != nullptr);
|
|
|
|
tapLength = sizeof(tapHeader) + 8 + 8;
|
|
// Note: tshark expects specific lengths for TLVs.
|
|
// FCS TLV: Type(2), Length(2), Value(1), Padding(3) -> Total 8 bytes. Value length = 1.
|
|
// Channel TLV: Type(2), Length(2), Channel(2), Page(1), Padding(1) -> Total 8 bytes. Value length = 3.
|
|
|
|
packetLen = tapLength + aFrame.mLength;
|
|
paddedLen = (packetLen + 3) & ~3u;
|
|
blockTotalLength = sizeof(epb) + paddedLen + sizeof(uint32_t);
|
|
|
|
ClearAllBytes(epb);
|
|
epb.mBlockType = LittleEndian::HostSwap(kPcapngEpbType);
|
|
epb.mBlockTotalLength = LittleEndian::HostSwap(blockTotalLength);
|
|
epb.mInterfaceId = LittleEndian::HostSwap(0u);
|
|
epb.mTimestampHigh = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs >> 32));
|
|
epb.mTimestampLow = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs & 0xffffffff));
|
|
epb.mCapturedLen = LittleEndian::HostSwap(packetLen);
|
|
epb.mOriginalLen = LittleEndian::HostSwap(packetLen);
|
|
VerifyOrExit(fwrite(&epb, sizeof(epb), 1, mFile) == 1, Close());
|
|
|
|
ClearAllBytes(tapHeader);
|
|
tapHeader.mVersion = LittleEndian::HostSwap(static_cast<uint16_t>(kTapVersion));
|
|
tapHeader.mLength = LittleEndian::HostSwap(static_cast<uint16_t>(tapLength));
|
|
VerifyOrExit(fwrite(&tapHeader, sizeof(tapHeader), 1, mFile) == 1, Close());
|
|
|
|
// FCS TLV
|
|
uint8_t fcsTlv[8];
|
|
ClearAllBytes(fcsTlv);
|
|
LittleEndian::WriteUint16(kTapFcsType, &fcsTlv[0]);
|
|
LittleEndian::WriteUint16(kTapFcsLength, &fcsTlv[2]);
|
|
fcsTlv[4] = kTapFcsValue;
|
|
VerifyOrExit(fwrite(fcsTlv, sizeof(fcsTlv), 1, mFile) == 1, Close());
|
|
|
|
// Channel TLV
|
|
uint8_t channelTlv[8];
|
|
ClearAllBytes(channelTlv);
|
|
LittleEndian::WriteUint16(kTapChannelType, &channelTlv[0]);
|
|
LittleEndian::WriteUint16(kTapChannelLength, &channelTlv[2]);
|
|
LittleEndian::WriteUint16(aFrame.mChannel, &channelTlv[4]);
|
|
channelTlv[6] = kTapChannelPage;
|
|
VerifyOrExit(fwrite(channelTlv, sizeof(channelTlv), 1, mFile) == 1, Close());
|
|
|
|
VerifyOrExit(fwrite(aFrame.mPsdu, aFrame.mLength, 1, mFile) == 1, Close());
|
|
|
|
if (paddedLen > packetLen)
|
|
{
|
|
VerifyOrExit(fwrite(&padding, paddedLen - packetLen, 1, mFile) == 1, Close());
|
|
}
|
|
|
|
VerifyOrExit(fwrite(&epb.mBlockTotalLength, sizeof(epb.mBlockTotalLength), 1, mFile) == 1, Close());
|
|
|
|
VerifyOrExit(fflush(mFile) == 0, Close());
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Pcap::WritePacket(const InfraIf::LinkLayerAddress &aSrcAddr,
|
|
const InfraIf::LinkLayerAddress &aDstAddr,
|
|
const uint8_t *aBuffer,
|
|
uint16_t aLength,
|
|
uint64_t aTimeUs)
|
|
{
|
|
Epb epb;
|
|
|
|
OT_TOOL_PACKED_BEGIN
|
|
struct EthernetHeader
|
|
{
|
|
uint8_t mDst[6];
|
|
uint8_t mSrc[6];
|
|
uint16_t mType;
|
|
} OT_TOOL_PACKED_END;
|
|
EthernetHeader ethHeader;
|
|
|
|
uint32_t packetLen;
|
|
uint32_t paddedLen;
|
|
uint32_t blockTotalLength;
|
|
uint32_t padding = 0;
|
|
|
|
VerifyOrExit(mFile != nullptr);
|
|
|
|
ClearAllBytes(ethHeader);
|
|
VerifyOrQuit(aDstAddr.GetLength() >= sizeof(ethHeader.mDst),
|
|
"Destination MAC address length is less than expected");
|
|
memcpy(ethHeader.mDst, aDstAddr.GetBytes(), sizeof(ethHeader.mDst));
|
|
VerifyOrQuit(aSrcAddr.GetLength() >= sizeof(ethHeader.mSrc), "Source MAC address length is less than expected");
|
|
memcpy(ethHeader.mSrc, aSrcAddr.GetBytes(), sizeof(ethHeader.mSrc));
|
|
ethHeader.mType = BigEndian::HostSwap16(kEtherTypeIPv6);
|
|
|
|
packetLen = sizeof(ethHeader) + aLength;
|
|
paddedLen = (packetLen + 3) & ~3u;
|
|
blockTotalLength = sizeof(epb) + paddedLen + sizeof(uint32_t);
|
|
|
|
ClearAllBytes(epb);
|
|
epb.mBlockType = LittleEndian::HostSwap(kPcapngEpbType);
|
|
epb.mBlockTotalLength = LittleEndian::HostSwap(blockTotalLength);
|
|
epb.mInterfaceId = LittleEndian::HostSwap(1u); // Use Interface ID 1 for Ethernet
|
|
epb.mTimestampHigh = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs >> 32));
|
|
epb.mTimestampLow = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs & 0xffffffff));
|
|
epb.mCapturedLen = LittleEndian::HostSwap(packetLen);
|
|
epb.mOriginalLen = LittleEndian::HostSwap(packetLen);
|
|
VerifyOrExit(fwrite(&epb, sizeof(epb), 1, mFile) == 1, Close());
|
|
|
|
VerifyOrExit(fwrite(ðHeader, sizeof(ethHeader), 1, mFile) == 1, Close());
|
|
VerifyOrExit(fwrite(aBuffer, aLength, 1, mFile) == 1, Close());
|
|
|
|
if (paddedLen > packetLen)
|
|
{
|
|
VerifyOrExit(fwrite(&padding, paddedLen - packetLen, 1, mFile) == 1, Close());
|
|
}
|
|
|
|
VerifyOrExit(fwrite(&epb.mBlockTotalLength, sizeof(epb.mBlockTotalLength), 1, mFile) == 1, Close());
|
|
|
|
VerifyOrExit(fflush(mFile) == 0, Close());
|
|
|
|
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
|