diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 00cf83177..ed26eb82b 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(ot-nexus-platform platform/nexus_mdns.cpp platform/nexus_misc.cpp platform/nexus_node.cpp + platform/nexus_pcap.cpp platform/nexus_radio.cpp platform/nexus_settings.cpp platform/nexus_trel.cpp diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index 34a18917b..71f7450cb 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -27,6 +27,9 @@ */ #include "nexus_core.hpp" + +#include + #include "nexus_node.hpp" namespace ot { @@ -40,11 +43,20 @@ Core::Core(void) , mCurNodeId(0) , mPendingAction(false) { + const char *pcapFile; + VerifyOrQuit(!sInUse); sCore = this; sInUse = true; mNextAlarmTime = mNow.GetDistantFuture(); + + pcapFile = getenv("OT_NEXUS_PCAP_FILE"); + + if ((pcapFile != nullptr) && (pcapFile[0] != '\0')) + { + mPcap.Open(pcapFile); + } } Core::~Core(void) { sInUse = false; } @@ -134,6 +146,8 @@ void Core::ProcessRadio(Node &aNode) ackRequested = aNode.mRadio.mTxFrame.GetAckRequest(); + mPcap.WriteFrame(aNode.mRadio.mTxFrame, mNow.GetValue() * 1000ull); + otPlatRadioTxStarted(&aNode.GetInstance(), &aNode.mRadio.mTxFrame); for (Node &rxNode : mNodes) @@ -187,16 +201,15 @@ void Core::ProcessRadio(Node &aNode) if (ackMode != kNoAck) { - Mac::TxFrame ackFrame; - uint8_t ackPsdu[Mac::Frame::kImmAckLength]; - - ClearAllBytes(ackFrame); - ackFrame.mPsdu = ackPsdu; + Radio::Frame ackFrame; ackFrame.GenerateImmAck( static_cast(static_cast(aNode.mRadio.mTxFrame)), (ackMode == kSendAckFramePending)); + ackFrame.UpdateFcs(); + mPcap.WriteFrame(ackFrame, mNow.GetValue() * 1000ull); + otPlatRadioTxDone(&aNode.GetInstance(), &aNode.mRadio.mTxFrame, &ackFrame, kErrorNone); } else diff --git a/tests/nexus/platform/nexus_core.hpp b/tests/nexus/platform/nexus_core.hpp index a45f5fe71..11ed2c36a 100644 --- a/tests/nexus/platform/nexus_core.hpp +++ b/tests/nexus/platform/nexus_core.hpp @@ -33,6 +33,7 @@ #include "instance/instance.hpp" #include "nexus_alarm.hpp" +#include "nexus_pcap.hpp" #include "nexus_radio.hpp" #include "nexus_utils.hpp" @@ -85,6 +86,7 @@ private: static bool sInUse; OwningList mNodes; + Pcap mPcap; uint16_t mCurNodeId; bool mPendingAction; TimeMilli mNow; diff --git a/tests/nexus/platform/nexus_pcap.cpp b/tests/nexus/platform/nexus_pcap.cpp new file mode 100644 index 000000000..b7594bd61 --- /dev/null +++ b/tests/nexus/platform/nexus_pcap.cpp @@ -0,0 +1,165 @@ +/* + * 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 "common/clearable.hpp" +#include "common/code_utils.hpp" +#include "common/encoding.hpp" + +namespace ot { +namespace Nexus { + +Pcap::Pcap(void) + : mFile(nullptr) +{ +} + +Pcap::~Pcap(void) { Close(); } + +void Pcap::Open(const char *aFilename) +{ + OT_TOOL_PACKED_BEGIN + struct PcapHeader + { + uint32_t mMagicNumber; + uint16_t mVersionMajor; + uint16_t mVersionMinor; + int32_t mThisZone; + uint32_t mSigFigs; + uint32_t mSnapLen; + uint32_t mNetwork; + } OT_TOOL_PACKED_END header; + + Close(); + + mFile = fopen(aFilename, "wb"); + VerifyOrExit(mFile != nullptr); + + ClearAllBytes(header); + header.mMagicNumber = LittleEndian::HostSwap(kPcapMagicNumber); + header.mVersionMajor = LittleEndian::HostSwap(kPcapVersionMajor); + header.mVersionMinor = LittleEndian::HostSwap(kPcapVersionMinor); + header.mSnapLen = LittleEndian::HostSwap(kPcapSnapLen); + header.mNetwork = LittleEndian::HostSwap(kPcapDlt154Tap); + + VerifyOrExit(fwrite(&header, sizeof(header), 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) +{ + OT_TOOL_PACKED_BEGIN + struct PcapRecordHeader + { + uint32_t mTsSec; + uint32_t mTsUsec; + uint32_t mInclLen; + uint32_t mOrigLen; + } OT_TOOL_PACKED_END recordHeader; + + OT_TOOL_PACKED_BEGIN + struct TapHeader + { + uint8_t mVersion; + uint8_t mReserved; + uint16_t mLength; + } OT_TOOL_PACKED_END tapHeader; + + OT_TOOL_PACKED_BEGIN + struct TapFcsTlv + { + uint16_t mType; + uint16_t mLength; + uint8_t mValue; + uint8_t mPadding[3]; + } OT_TOOL_PACKED_END fcsTlv; + + OT_TOOL_PACKED_BEGIN + struct TapChannelTlv + { + uint16_t mType; + uint16_t mLength; + uint8_t mPage; + uint16_t mChannel; + uint8_t mPadding[1]; + } OT_TOOL_PACKED_END channelTlv; + + uint32_t tapLength; + + VerifyOrExit(mFile != nullptr); + + tapLength = sizeof(tapHeader) + sizeof(fcsTlv) + sizeof(channelTlv); + + ClearAllBytes(recordHeader); + recordHeader.mTsSec = LittleEndian::HostSwap(static_cast(aTimeUs / 1000000)); + recordHeader.mTsUsec = LittleEndian::HostSwap(static_cast(aTimeUs % 1000000)); + recordHeader.mInclLen = LittleEndian::HostSwap(tapLength + aFrame.mLength); + recordHeader.mOrigLen = recordHeader.mInclLen; + VerifyOrExit(fwrite(&recordHeader, sizeof(recordHeader), 1, mFile) == 1, Close()); + + ClearAllBytes(tapHeader); + tapHeader.mVersion = LittleEndian::HostSwap(kTapVersion); + tapHeader.mLength = LittleEndian::HostSwap(static_cast(tapLength)); + VerifyOrExit(fwrite(&tapHeader, sizeof(tapHeader), 1, mFile) == 1, Close()); + + ClearAllBytes(fcsTlv); + fcsTlv.mType = LittleEndian::HostSwap(kTapFcsType); + fcsTlv.mLength = LittleEndian::HostSwap(kTapFcsLength); + fcsTlv.mValue = LittleEndian::HostSwap(kTapFcsValue); + VerifyOrExit(fwrite(&fcsTlv, sizeof(fcsTlv), 1, mFile) == 1, Close()); + + ClearAllBytes(channelTlv); + channelTlv.mType = LittleEndian::HostSwap(kTapChannelType); + channelTlv.mLength = LittleEndian::HostSwap(kTapChannelLength); + channelTlv.mPage = LittleEndian::HostSwap(kTapChannelPage); + channelTlv.mChannel = LittleEndian::HostSwap(aFrame.mChannel); + VerifyOrExit(fwrite(&channelTlv, sizeof(channelTlv), 1, mFile) == 1, Close()); + + VerifyOrExit(fwrite(aFrame.mPsdu, aFrame.mLength, 1, mFile) == 1, Close()); + VerifyOrExit(fflush(mFile) == 0, Close()); + +exit: + return; +} + +} // namespace Nexus +} // namespace ot diff --git a/tests/nexus/platform/nexus_pcap.hpp b/tests/nexus/platform/nexus_pcap.hpp new file mode 100644 index 000000000..128681fcc --- /dev/null +++ b/tests/nexus/platform/nexus_pcap.hpp @@ -0,0 +1,89 @@ +/* + * 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. + */ + +#ifndef OT_NEXUS_PLATFORM_NEXUS_PCAP_HPP_ +#define OT_NEXUS_PLATFORM_NEXUS_PCAP_HPP_ + +#include +#include + +#include + +namespace ot { +namespace Nexus { + +class Pcap +{ +public: + Pcap(void); + ~Pcap(void); + + /** + * Opens a pcap file. + * + * @param[in] aFilename The filename to open. + */ + void Open(const char *aFilename); + + /** + * Closes the pcap file. + */ + void Close(void); + + /** + * Writes a frame to the pcap file. + * + * @param[in] aFrame The frame to write. + * @param[in] aTimeUs The timestamp in microseconds. + */ + void WriteFrame(const otRadioFrame &aFrame, uint64_t aTimeUs); + +private: + static constexpr uint32_t kPcapMagicNumber = 0xa1b2c3d4; + static constexpr uint16_t kPcapVersionMajor = 2; + static constexpr uint16_t kPcapVersionMinor = 4; + static constexpr uint32_t kPcapSnapLen = 65535; + static constexpr uint32_t kPcapDlt154Tap = 283; + + static constexpr uint8_t kTapVersion = 0; + + static constexpr uint16_t kTapFcsType = 0; + static constexpr uint16_t kTapFcsLength = 1; + static constexpr uint8_t kTapFcsValue = 1; // 16-bit CRC + + static constexpr uint16_t kTapChannelType = 3; + static constexpr uint16_t kTapChannelLength = 3; + static constexpr uint8_t kTapChannelPage = 0; + + FILE *mFile; +}; + +} // namespace Nexus +} // namespace ot + +#endif // OT_NEXUS_PLATFORM_NEXUS_PCAP_HPP_ diff --git a/tests/nexus/platform/nexus_radio.hpp b/tests/nexus/platform/nexus_radio.hpp index 9e14cf89f..a87646d5e 100644 --- a/tests/nexus/platform/nexus_radio.hpp +++ b/tests/nexus/platform/nexus_radio.hpp @@ -50,7 +50,7 @@ struct Radio static constexpr State kStateReceive = OT_RADIO_STATE_RECEIVE; static constexpr State kStateTransmit = OT_RADIO_STATE_TRANSMIT; - struct Frame : public Mac::Frame + struct Frame : public Mac::TxFrame { Frame(void); explicit Frame(const Frame &aFrame);