From ea06b353e6ec7a367fc8aed6460ee346c82a505e Mon Sep 17 00:00:00 2001 From: Puneet Kumar Date: Thu, 30 May 2024 14:54:51 -0700 Subject: [PATCH] wifi: (merges !2004) Add Wi-Fi channel occupancy statistics helper --- RELEASE_NOTES.md | 1 + src/wifi/CMakeLists.txt | 3 + src/wifi/doc/source/wifi-references.rst | 2 + src/wifi/doc/source/wifi-testing.rst | 5 + src/wifi/doc/source/wifi-user.rst | 56 ++ src/wifi/examples/CMakeLists.txt | 10 + src/wifi/examples/wifi-co-trace-example.cc | 221 ++++++ src/wifi/helper/wifi-co-trace-helper.cc | 387 ++++++++++ src/wifi/helper/wifi-co-trace-helper.h | 217 ++++++ src/wifi/test/wifi-co-trace-helper-test.cc | 788 +++++++++++++++++++++ 10 files changed, 1690 insertions(+) create mode 100644 src/wifi/examples/wifi-co-trace-example.cc create mode 100644 src/wifi/helper/wifi-co-trace-helper.cc create mode 100644 src/wifi/helper/wifi-co-trace-helper.h create mode 100644 src/wifi/test/wifi-co-trace-helper-test.cc diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b5e15bdfc..d4beb43c5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -46,6 +46,7 @@ The required Doxygen version for documentation generation is version 1.11. - (wifi) Added a new `MainPhySwitch` trace source to EmlsrManager, which is fired when the main PHY switches channel to operate on another link and provides information about the reason for starting the switch. - (build) Scan for contrib modules in `ns-3-external-contrib` directory, at the same level of the ns-3 directory (e.g. `./ns-3-dev/../ns-3-external-contrib/`). - (wifi) Add support for exchanging 802.11be Multi-Link Probe Request frames. Currently, the default association manager does not instruct the MAC to transmit a Multi-Link Probe Request frame, though. +- (wifi) 2004! - Add Wi-Fi channel occupancy statistics helper - (wifi) 2009! - Added WifiTxStatsHelper for Wi-Fi MAC-level tracing. ### Bugs fixed diff --git a/src/wifi/CMakeLists.txt b/src/wifi/CMakeLists.txt index 3e130db9d..f0458dcd5 100644 --- a/src/wifi/CMakeLists.txt +++ b/src/wifi/CMakeLists.txt @@ -8,6 +8,7 @@ endif() set(source_files helper/athstats-helper.cc helper/spectrum-wifi-helper.cc + helper/wifi-co-trace-helper.cc helper/wifi-helper.cc helper/wifi-mac-helper.cc helper/wifi-radio-energy-model-helper.cc @@ -178,6 +179,7 @@ set(source_files set(header_files helper/athstats-helper.h helper/spectrum-wifi-helper.h + helper/wifi-co-trace-helper.h helper/wifi-helper.h helper/wifi-mac-helper.h helper/wifi-radio-energy-model-helper.h @@ -370,6 +372,7 @@ build_lib( test/spectrum-wifi-phy-test.cc test/tx-duration-test.cc test/wifi-aggregation-test.cc + test/wifi-co-trace-helper-test.cc test/wifi-dynamic-bw-op-test.cc test/wifi-eht-info-elems-test.cc test/wifi-emlsr-test.cc diff --git a/src/wifi/doc/source/wifi-references.rst b/src/wifi/doc/source/wifi-references.rst index 18fcea458..07506be62 100644 --- a/src/wifi/doc/source/wifi-references.rst +++ b/src/wifi/doc/source/wifi-references.rst @@ -68,3 +68,5 @@ References .. [corbet2012] \ J. Corbet, "TCP Small Queues", `LWN.net, July 17, 2012 `__ .. [grazia2022] \ C. Grazia, N. Patriciello, T. Hoiland-Jorgensen, M. Klapez and M. Casoni, "Aggregating Without Bloating: Hard Times for TCP on Wi-Fi", IEEE/ACM Transactions on Networking, Vol. 30, No.5, October 2022. + +.. [kumar2025comsnets] \ P. Kumar, J. Kulshrestha, M. Maity and S. Roy, "Use of Channel Occupancy for Multi Link WiFi 7 Scheduler Design in ns-3" 2025 17th International Conference on COMmunication Systems & NETworkS (COMSNETS), Bengaluru, India, 2025, to appear. diff --git a/src/wifi/doc/source/wifi-testing.rst b/src/wifi/doc/source/wifi-testing.rst index eba46048b..12f827058 100644 --- a/src/wifi/doc/source/wifi-testing.rst +++ b/src/wifi/doc/source/wifi-testing.rst @@ -300,3 +300,8 @@ The implementation of the OFDMA support has been validated against a theoretical A preliminary evaluation of the usage of OFDMA in 802.11ax, in terms of latency in non-saturated conditions, throughput in saturated conditions and transmission range with UL OFDMA, is provided in [avallone2021wcm]_ . + +Channel Occupancy Helper Testing +******************************** + +The Channel Occupancy helper (WifiCoTraceHelper class) has been tested by comparing the occupancy results for single packet transmissions of various sizes (one, two, or three symbols) with the results predicted by offline calculation of the expected values. Additionally, the helper has been validated in saturated traffic conditions as described in the research publication "Use of Channel Occupancy for Multi Link WiFi 7 Scheduler Design in ns-3" to be presented at COMSNETS 25 [kumar2025comsnets]_ . diff --git a/src/wifi/doc/source/wifi-user.rst b/src/wifi/doc/source/wifi-user.rst index 1e4598ae1..bd7c22bd8 100644 --- a/src/wifi/doc/source/wifi-user.rst +++ b/src/wifi/doc/source/wifi-user.rst @@ -1254,6 +1254,62 @@ will end up in one of the two data structures. The example program ``src/wifi/examples/wifi-bianchi.cc`` provides an example use of this helper, by setting the program option ``--useTxHelper`` to true. +WifiCoTraceHelper +================= + +The ``WifiCoTraceHelper`` (channel occupancy trace helper) can be used to collect statistics of Wi-Fi channel occupancy, as observed from the perspective of the WifiPhy state of devices or links (in the case of multi-link devices). The ``WifiPhyStateHelper`` tracks the state of the WifiPhy corresponding to whether it is in idle, transmitting, receiving, or some other state. This helper object can be added to ns-3 Wi-Fi simulations to track the duration and percentage of time spent in each state. + +Wi-Fi channel access relies also on additional busy states that conceptually reside in the MAC layer, corresponding to the Network Allocation Vector (NAV). The idle states reported herein correspond to the PHY (physical) carrier sense state and not the MAC (virtual) carrier sense state. + +In case of an EMLSR device, PHY objects keep switching among links. This helper aggregates a PHY duration to the link it is operating on at that instant. Be aware that switching PHY objects for an EMLSR device might result in unexpected statistics. For example, if auxiliary PHY is configured to not switch in EMLSR configurations then a link could have no PHY object operating on it intermittently. As a result, the total recorded duration on all links would not be same which does not occur for non-EMLSR devices. + +This helper can be added to a simulation program with statements such as the following: + +.. sourcecode:: cpp + + WifiCoTraceHelper coHelper{Seconds(1.0), Seconds(11.0)}; + +The above statement, if placed just prior to the call to ``Simulator::Run(),`` +will declare a helper object and configure it to collect statistics between +one and eleven seconds. However, WifiNetDevices must still be added, such +as the following sample code: + +.. sourcecode:: cpp + + coHelper.Enable(nodeOrDeviceContainer); + +Then finally, print the results after ``Simulator::Run()`` returns: + +.. sourcecode:: cpp + + coHelper.PrintStatistics(std::cout); + +This will print output such as: + +.. sourcecode:: text + + ---- COT for AP:0 ---- + Showing duration by states: + IDLE: +5.69s (56.93%) + CCA_BUSY: +1.18s (11.80%) + TX: +301.90ms (3.02%) + RX: +2.83s (28.25%) + + ---- COT for STA0:0 ---- + Showing duration by states: + IDLE: +5.71s (57.06%) + CCA_BUSY: +377.52ms (3.78%) + TX: +1.63s (16.31%) + RX: +2.29s (22.85%) + +You can export statistics from this helper for use cases such as printing in your own formatted statements: + +.. sourcecode:: cpp + + auto records = coHelper.GetDeviceRecords(); + +You can refer an example program in ``src/wifi/examples/wifi-co-trace-example.cc`` on how to use these APIs. + HT configuration ================ diff --git a/src/wifi/examples/CMakeLists.txt b/src/wifi/examples/CMakeLists.txt index c47e803bb..713e11607 100644 --- a/src/wifi/examples/CMakeLists.txt +++ b/src/wifi/examples/CMakeLists.txt @@ -66,3 +66,13 @@ build_lib_example( ${libinternet} ${libmobility} ) + +build_lib_example( + NAME wifi-co-trace-example + SOURCE_FILES wifi-co-trace-example.cc + LIBRARIES_TO_LINK + ${libwifi} + ${libinternet} + ${libmobility} + ${libapplications} +) diff --git a/src/wifi/examples/wifi-co-trace-example.cc b/src/wifi/examples/wifi-co-trace-example.cc new file mode 100644 index 000000000..ceb3c6f4f --- /dev/null +++ b/src/wifi/examples/wifi-co-trace-example.cc @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2024 University of Washington (updated to 802.11ax standard) + * Copyright (c) 2009 The Boeing Company + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +// The purpose of this example is to illustrate basic use of the +// WifiCoTraceHelper on a simple example program. +// +// This script configures four 802.11ax Wi-Fi STAs on a YansWifiChannel, +// with devices in infrastructure mode, and each STA sends a saturating load +// of UDP datagrams to the AP for a specified simulation duration. A simple +// free-space path loss (Friis) propagation loss model is configured. +// The lowest MCS ("HeMcs0") value is configured. +// +// At the end of the simulation, a channel occupancy report is printed for +// each STA and for the AP. There are two program options: +// -- duration: +// -- useDifferentAc: (true or false) +// +// If 'useDifferentAc' has the value false, all STAs will have the same EDCA parameters +// for best effort) and their channel utilization (the TX time output of the +// channel access helper) will be close to equal. If 'useDifferentAc' is true, +// then two of the four STAs will instead be configured to use the voice +// access category, and channel utilization will be different due to the +// different EDCA parameters. + +#include "ns3/boolean.h" +#include "ns3/command-line.h" +#include "ns3/config.h" +#include "ns3/double.h" +#include "ns3/internet-stack-helper.h" +#include "ns3/ipv4-address-helper.h" +#include "ns3/log.h" +#include "ns3/mobility-helper.h" +#include "ns3/mobility-model.h" +#include "ns3/names.h" +#include "ns3/neighbor-cache-helper.h" +#include "ns3/on-off-helper.h" +#include "ns3/packet-sink-helper.h" +#include "ns3/ssid.h" +#include "ns3/string.h" +#include "ns3/uinteger.h" +#include "ns3/wifi-co-trace-helper.h" +#include "ns3/wifi-phy-rx-trace-helper.h" +#include "ns3/yans-wifi-channel.h" +#include "ns3/yans-wifi-helper.h" + +using namespace ns3; + +NS_LOG_COMPONENT_DEFINE("WifiCoTraceExample"); + +// Function for runtime manual ARP configuration +void +PopulateNeighborCache() +{ + NeighborCacheHelper neighborCache; + neighborCache.PopulateNeighborCache(); +} + +int +main(int argc, char* argv[]) +{ + bool useDifferentAc = false; + Time duration{Seconds(10)}; + double distance = 1; // meters + + CommandLine cmd(__FILE__); + cmd.AddValue("useDifferentAc", + "Uses VO AC on 2 STAs and BE on rest if true. Uses BE AC on all 4 STAs if false.", + useDifferentAc); + cmd.AddValue("duration", "Duration of data transfer", duration); + cmd.Parse(argc, argv); + + NodeContainer apNode(1); + Names::Add("AP", apNode.Get(0)); + NodeContainer staNodes(4); + Names::Add("STA0", staNodes.Get(0)); + Names::Add("STA1", staNodes.Get(1)); + Names::Add("STA2", staNodes.Get(2)); + Names::Add("STA3", staNodes.Get(3)); + + MobilityHelper mobility; + Ptr positionAlloc = CreateObject(); + positionAlloc->Add(Vector(0.0, 0.0, 0.0)); + mobility.SetPositionAllocator(positionAlloc); + mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel"); + mobility.Install(apNode); + positionAlloc = CreateObject(); + positionAlloc->Add(Vector(distance, 0.0, 0.0)); + positionAlloc->Add(Vector(0.0, distance, 0.0)); + positionAlloc->Add(Vector(0.0, -distance, 0.0)); + positionAlloc->Add(Vector(-distance, 0.0, 0.0)); + mobility.Install(staNodes); + + WifiHelper wifi; + wifi.SetStandard(WIFI_STANDARD_80211ax); + + YansWifiPhyHelper wifiPhy; + YansWifiChannelHelper wifiChannel; + wifiChannel.SetPropagationDelay("ns3::ConstantSpeedPropagationDelayModel"); + wifiChannel.AddPropagationLoss("ns3::FriisPropagationLossModel"); + wifiPhy.SetChannel(wifiChannel.Create()); + + // Add a mac and disable rate control + WifiMacHelper wifiMac; + wifi.SetRemoteStationManager("ns3::ConstantRateWifiManager", + "DataMode", + StringValue("HeMcs0"), + "ControlMode", + StringValue("HeMcs0")); + + // Setup the rest of the MAC + Ssid ssid = Ssid("wifi-default"); + // setup AP to beacon roughly once per second (must be a multiple of 1024 us) + wifiMac.SetType("ns3::ApWifiMac", + "Ssid", + SsidValue(ssid), + "QosSupported", + BooleanValue(true), + "BeaconInterval", + TimeValue(MilliSeconds(1024))); + NetDeviceContainer apDevice = wifi.Install(wifiPhy, wifiMac, apNode); + + // setup STA and disable the possible loss of association due to missed beacons + wifiMac.SetType("ns3::StaWifiMac", + "Ssid", + SsidValue(ssid), + "QosSupported", + BooleanValue(true), + "MaxMissedBeacons", + UintegerValue(std::numeric_limits::max())); + NetDeviceContainer staDevices = wifi.Install(wifiPhy, wifiMac, staNodes); + + NetDeviceContainer allDevices; + allDevices.Add(apDevice); + allDevices.Add(staDevices); + + InternetStackHelper internet; + internet.Install(apNode); + internet.Install(staNodes); + + Ipv4AddressHelper ipv4; + ipv4.SetBase("10.1.1.0", "255.255.255.0"); + Ipv4InterfaceContainer i = ipv4.Assign(allDevices); + + uint16_t portNumber = 9; + std::vector tosValues = {0x70, 0x28, 0xb8, 0xc0}; // AC_BE, AC_BK, AC_VI, AC_VO + auto ipv4ap = apNode.Get(0)->GetObject(); + const auto address = ipv4ap->GetAddress(1, 0).GetLocal(); + + ApplicationContainer sourceApplications; + ApplicationContainer sinkApplications; + for (uint32_t i = 0; i < 4; i++) + { + InetSocketAddress sinkAddress(address, portNumber + i); + PacketSinkHelper packetSinkHelper("ns3::UdpSocketFactory", sinkAddress); + sinkApplications.Add(packetSinkHelper.Install(apNode.Get(0))); + OnOffHelper onOffHelper("ns3::UdpSocketFactory", sinkAddress); + onOffHelper.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]")); + onOffHelper.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]")); + onOffHelper.SetAttribute("DataRate", DataRateValue(2000000)); // bits/sec + onOffHelper.SetAttribute("PacketSize", UintegerValue(1472)); // bytes + if (!useDifferentAc) + { + onOffHelper.SetAttribute("Tos", UintegerValue(tosValues[0])); // AC_BE + } + else + { + onOffHelper.SetAttribute("Tos", + UintegerValue(tosValues[3 * (i % 2)])); // AC_BE and AC_VO + } + sourceApplications.Add(onOffHelper.Install(staNodes.Get(i))); + } + + sinkApplications.Start(Seconds(0.0)); + sinkApplications.Stop(Seconds(1.0) + duration + MilliSeconds(20)); + sourceApplications.Start(Seconds(1.0)); + sourceApplications.Stop(Seconds(1.0) + duration); + + // Use the NeighborCacheHelper to avoid ARP messages (ARP replies, since they are unicast, + // count in the statistics. The cache operation must be scheduled after WifiNetDevices are + // started, until issue #851 is fixed. The indirection through a normal function is + // necessary because NeighborCacheHelper::PopulateNeighborCache() is overloaded + Simulator::Schedule(Seconds(0.99), &PopulateNeighborCache); + + WifiCoTraceHelper wifiCoTraceHelper(Seconds(1), Seconds(1) + duration); + wifiCoTraceHelper.Enable(allDevices); + + Simulator::Stop(duration + Seconds(2)); + Simulator::Run(); + + // The following provide some examples of how to access and print the trace helper contents. + std::cout << "*** Print statistics for all nodes using built-in print method:" << std::endl; + wifiCoTraceHelper.PrintStatistics(std::cout); + + std::cout << "*** Print the statistics in your own way. Here, just sum the STAs total TX time:" + << std::endl + << std::endl; + + auto records = wifiCoTraceHelper.GetDeviceRecords(); + Time sumStaTxTime; + for (const auto& it : records) + { + if (it.m_nodeId > 0) + { + const auto it2 = it.m_linkStateDurations.at(0).find(WifiPhyState::TX); + if (it2 != it.m_linkStateDurations.at(0).end()) + { + sumStaTxTime += it2->second; + } + } + } + + std::cout << "Sum of STA time in TX state is " << sumStaTxTime.As(Time::S) << std::endl; + + Simulator::Destroy(); + + return 0; +} diff --git a/src/wifi/helper/wifi-co-trace-helper.cc b/src/wifi/helper/wifi-co-trace-helper.cc new file mode 100644 index 000000000..069b36ca8 --- /dev/null +++ b/src/wifi/helper/wifi-co-trace-helper.cc @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2024 Indraprastha Institute of Information Technology Delhi + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "wifi-co-trace-helper.h" + +#include "ns3/assert.h" +#include "ns3/log.h" +#include "ns3/names.h" +#include "ns3/net-device-container.h" +#include "ns3/node-container.h" +#include "ns3/node-list.h" +#include "ns3/node.h" +#include "ns3/pointer.h" +#include "ns3/wifi-mac.h" +#include "ns3/wifi-net-device.h" +#include "ns3/wifi-phy-state-helper.h" +#include "ns3/wifi-phy.h" + +#include +#include +#include +#include + +namespace ns3 +{ + +NS_LOG_COMPONENT_DEFINE("WifiCoTraceHelper"); + +WifiCoTraceHelper::WifiCoTraceHelper() +{ + NS_LOG_FUNCTION(this); +} + +WifiCoTraceHelper::WifiCoTraceHelper(Time startTime, Time stopTime) +{ + NS_LOG_FUNCTION(this << startTime.As(Time::S) << stopTime.As(Time::S)); + NS_ASSERT_MSG(startTime <= stopTime, + "Invalid Start: " << startTime << " and Stop: " << stopTime << " Time"); + + m_startTime = startTime; + m_stopTime = stopTime; +} + +void +WifiCoTraceHelper::Start(Time start) +{ + NS_LOG_FUNCTION(this << start.As(Time::S)); + NS_ASSERT_MSG(start <= m_stopTime, + "Invalid Start: " << start << " and Stop: " << m_stopTime << " Time"); + NS_ASSERT_MSG(start >= Simulator::Now(), + "Invalid Start: " << start << " less than Now(): " << Simulator::Now()); + m_startTime = start; +} + +void +WifiCoTraceHelper::Stop(Time stop) +{ + NS_LOG_FUNCTION(this << stop.As(Time::S)); + NS_ASSERT_MSG(m_startTime <= stop, + "Invalid Start: " << m_startTime << " and Stop: " << stop << " Time"); + NS_ASSERT_MSG(stop >= Simulator::Now(), + "Invalid Stop: " << stop << " less than Now(): " << Simulator::Now()); + + m_stopTime = stop; +} + +void +WifiCoTraceHelper::Reset() +{ + NS_LOG_FUNCTION(this); + + for (auto& record : m_deviceRecords) + { + record.m_linkStateDurations.clear(); + } +} + +void +WifiCoTraceHelper::Enable(NodeContainer nodes) +{ + NS_LOG_FUNCTION(this << nodes.GetN()); + NetDeviceContainer netDevices; + for (uint32_t i = 0; i < nodes.GetN(); ++i) + { + for (uint32_t j = 0; j < nodes.Get(i)->GetNDevices(); ++j) + { + netDevices.Add(nodes.Get(i)->GetDevice(j)); + } + } + Enable(netDevices); +} + +void +WifiCoTraceHelper::Enable(NetDeviceContainer devices) +{ + NS_LOG_FUNCTION(this << devices.GetN()); + + for (uint32_t j = 0; j < devices.GetN(); ++j) + { + auto device = DynamicCast(devices.Get(j)); + if (!device) + { + NS_LOG_INFO("Ignoring deviceId: " << devices.Get(j)->GetIfIndex() << " on nodeId: " + << devices.Get(j)->GetNode()->GetId() + << " because it is not of type WifiNetDevice"); + continue; + } + const auto idx = m_numDevices++; + m_deviceRecords.emplace_back(device); + + for (uint32_t k = 0; k < device->GetNPhys(); ++k) + { + auto wifiPhyStateHelper = device->GetPhy(k)->GetState(); + auto linkCallback = + MakeCallback(&WifiCoTraceHelper::NotifyWifiPhyState, this).Bind(idx, k); + wifiPhyStateHelper->TraceConnectWithoutContext("State", linkCallback); + } + } +} + +void +WifiCoTraceHelper::PrintStatistics(std::ostream& os, Time::Unit unit) const +{ + NS_LOG_FUNCTION(this); + NS_ASSERT_MSG(m_deviceRecords.size() == m_numDevices, "m_deviceRecords size mismatch"); + + for (size_t i = 0; i < m_numDevices; ++i) + { + auto nodeName = m_deviceRecords[i].m_nodeName; + auto deviceName = m_deviceRecords[i].m_deviceName; + if (nodeName.empty()) + { + nodeName = std::to_string(m_deviceRecords[i].m_nodeId); + } + if (deviceName.empty()) + { + deviceName = std::to_string(m_deviceRecords[i].m_ifIndex); + } + auto numLinks = m_deviceRecords[i].m_linkStateDurations.size(); + if (numLinks == 1) + { + auto& statistics = m_deviceRecords[i].m_linkStateDurations.begin()->second; + os << "\n" + << "---- COT for " << nodeName << ":" << deviceName << " ----" + << "\n"; + PrintLinkStates(os, statistics, unit); + } + else if (numLinks > 1) + { + os << "\nDevice \"" << nodeName << ":" << deviceName + << "\" has statistics for multiple links: " + << "\n"; + for (auto& linkStates : m_deviceRecords[i].m_linkStateDurations) + { + os << "\n" + << "---- COT for " << nodeName << ":" << deviceName << "#Link" + << std::to_string(linkStates.first) << " ---" + << "\n"; + PrintLinkStates(os, linkStates.second, unit); + } + } + else + { + os << "\nDevice \"" << nodeName << ":" << deviceName << "\" has no statistics." + << "\n"; + } + } + os << "\n"; +} + +std::ostream& +WifiCoTraceHelper::PrintLinkStates(std::ostream& os, + const std::map& linkStates, + Time::Unit unit) const +{ + NS_LOG_FUNCTION(this); + os << "Showing duration by states: " + << "\n"; + + const auto percents = ComputePercentage(linkStates); + const auto showPercents = !percents.empty(); + + std::vector stateColumn{}; + std::vector durationColumn{}; + std::vector percentColumn{}; + + for (const auto& it : linkStates) + { + std::stringstream stateStream; + stateStream << it.first << ": "; + stateColumn.emplace_back(stateStream.str()); + + std::stringstream durationStream; + durationStream << std::showpoint << std::fixed << std::setprecision(2) + << it.second.As(unit); + durationColumn.emplace_back(durationStream.str()); + + if (showPercents) + { + std::stringstream percentStream; + percentStream << std::showpoint << std::fixed << std::setprecision(2) << " (" + << percents.at(it.first) << "%)"; + percentColumn.emplace_back(percentStream.str()); + } + } + + AlignDecimal(durationColumn); + if (showPercents) + { + AlignDecimal(percentColumn); + } + AlignWidth(stateColumn); + AlignWidth(durationColumn); + + for (size_t i = 0; i < stateColumn.size(); ++i) + { + os << stateColumn.at(i) << durationColumn.at(i); + if (showPercents) + { + os << percentColumn.at(i); + } + os << "\n"; + } + + return os; +} + +void +WifiCoTraceHelper::AlignDecimal(std::vector& column) const +{ + size_t maxPos = 0; + char decimal = '.'; + + for (auto& s : column) + { + size_t pos = s.find_first_of(decimal); + if (pos > maxPos) + { + maxPos = pos; + } + } + + for (auto& s : column) + { + auto padding = std::string(maxPos - s.find_first_of(decimal), ' '); + s = padding + s; + } +} + +void +WifiCoTraceHelper::AlignWidth(std::vector& column) const +{ + size_t maxWidth = 0; + + for (auto& s : column) + { + size_t width = s.length(); + if (width > maxWidth) + { + maxWidth = width; + } + } + + for (auto& s : column) + { + auto padding = std::string(maxWidth - s.length(), ' '); + s = s + padding; + } +} + +std::map +WifiCoTraceHelper::ComputePercentage(const std::map& linkStates) const +{ + NS_LOG_FUNCTION(this); + Time total; + for (const auto& it : linkStates) + { + total += it.second; + } + + if (total.IsZero()) + { + return {}; + } + + std::map percents; + for (const auto& it : linkStates) + { + percents[it.first] = it.second.GetDouble() * 100.0 / total.GetDouble(); + } + + return percents; +} + +const std::vector& +WifiCoTraceHelper::GetDeviceRecords() const +{ + return m_deviceRecords; +} + +void +WifiCoTraceHelper::NotifyWifiPhyState(std::size_t idx, + std::size_t phyId, + Time start, + Time duration, + WifiPhyState state) +{ + NS_LOG_FUNCTION(this << idx << phyId << start.As(Time::S) << duration.As(Time::US) << state); + NS_ASSERT_MSG(duration.IsPositive(), "Duration shouldn't be negative: " << duration.As()); + NS_ASSERT_MSG(idx < m_deviceRecords.size(), "Index out-of-bounds"); + + // Compute duration that overlaps with [m_startTime, m_stopTime] + const auto overlappingDuration = + ComputeOverlappingDuration(m_startTime, m_stopTime, start, start + duration); + + if (!overlappingDuration.IsZero()) + { + const auto nodeId = m_deviceRecords[idx].m_nodeId; + const auto deviceId = m_deviceRecords[idx].m_ifIndex; + const auto device = NodeList::GetNode(nodeId)->GetDevice(deviceId); + const auto wifiDevice = DynamicCast(device); + NS_ASSERT_MSG(wifiDevice, "Error, Device type is not WifiNetDevice."); + + auto linkId = wifiDevice->GetMac()->GetLinkForPhy(phyId); + + if (linkId.has_value()) + { + NS_LOG_INFO("Add device node " + << m_deviceRecords[idx].m_nodeId << " index " + << m_deviceRecords[idx].m_ifIndex << " linkId " << *linkId << " duration " + << overlappingDuration.As(Time::US) << " state " << state); + m_deviceRecords[idx].AddLinkMeasurement(*linkId, start, overlappingDuration, state); + } + else + { + NS_LOG_DEBUG("LinkId not found for phyId:" << phyId); + } + } +} + +Time +WifiCoTraceHelper::ComputeOverlappingDuration(Time start1, Time stop1, Time start2, Time stop2) +{ + const auto Zero{Seconds(0)}; + + NS_ASSERT_MSG(start1 >= Zero && stop1 >= Zero && start1 <= stop1, + "Interval: [" << start1 << "," << stop1 << "] is invalid."); + NS_ASSERT_MSG(start2 >= Zero && stop2 >= Zero && start2 <= stop2, + "Interval: [" << start2 << "," << stop2 << "] is invalid."); + + const auto maxStart = Max(start1, start2); + const auto minStop = Min(stop1, stop2); + const auto duration = minStop - maxStart; + + return duration > Zero ? duration : Zero; +} + +WifiCoTraceHelper::DeviceRecord::DeviceRecord(Ptr device) + : m_nodeId(device->GetNode()->GetId()), + m_ifIndex(device->GetIfIndex()) +{ + NS_LOG_FUNCTION(this << device); + if (!Names::FindName(device->GetNode()).empty()) + { + m_nodeName = Names::FindName(device->GetNode()); + } + if (!Names::FindName(device).empty()) + { + m_deviceName = Names::FindName(device); + } +} + +void +WifiCoTraceHelper::DeviceRecord::AddLinkMeasurement(size_t linkId, + Time start, + Time duration, + WifiPhyState state) +{ + NS_LOG_FUNCTION(this << linkId << start.As(Time::S) << duration.As(Time::S) << state); + auto& stateDurations = m_linkStateDurations[linkId]; + stateDurations[state] += duration; +} + +} // namespace ns3 diff --git a/src/wifi/helper/wifi-co-trace-helper.h b/src/wifi/helper/wifi-co-trace-helper.h new file mode 100644 index 000000000..c19762723 --- /dev/null +++ b/src/wifi/helper/wifi-co-trace-helper.h @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2024 Indraprastha Institute of Information Technology Delhi + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#ifndef WIFI_CO_HELPER_H +#define WIFI_CO_HELPER_H + +#include "ns3/callback.h" +#include "ns3/config.h" +#include "ns3/nstime.h" +#include "ns3/ptr.h" +#include "ns3/simulator.h" +#include "ns3/wifi-phy-state.h" + +#include +#include +#include +#include + +namespace ns3 +{ + +class WifiNetDevice; +class NodeContainer; +class NetDeviceContainer; + +/** + * @class WifiCoTraceHelper + * @brief Track channel occupancy durations for WifiNetDevice + * + * The WifiCoTraceHelper class tracks the duration that a particular WifiNetDevice is in + * different states. The states are defined by the ns-3 WifiPhyStateHelper and include states such + * as IDLE, CCA_BUSY, TX, and RX. The helper tracks these durations between a user-configured start + * and end time. At the end of a simulation, this helper can print out statistics on channel + * occupancy, and permits the export of an internal data structure to allow for custom printing or + * statistics handling. + * + * This helper supports both single-link devices and multi-link devices (MLD). + */ +class WifiCoTraceHelper +{ + public: + /** + * @struct DeviceRecord + * @brief Keeps track of channel occupancy statistics observed at a WifiNetDevice. + * + * Data structure to track durations of each WifiPhy state. Elements in m_linkStateDurations are + * indexed by "linkId". + */ + struct DeviceRecord + { + uint32_t m_nodeId; ///< Id of Node on which the WifiNetDevice is installed. + std::string m_nodeName; ///< Name of Node on which the WifiNetDevice is installed. It's + ///< empty if the name isn't configured. + uint32_t m_ifIndex; ///< Device Id of WifiNetDevice. + std::string m_deviceName; ///< Device name. It's empty if the name isn't configured. + + /** + * Duration statistics by link and state. LinkId is the key in the + * first map, and the WifiPhyState is the key to the second. + */ + std::map> m_linkStateDurations; + + /** + * Constructor. + * + * @param device The WifiNetDevice whose links will be monitored to collect statistics. + */ + DeviceRecord(Ptr device); + + /** + * Update the duration statistics for the provided linkId and state + * + * @param linkId Id of Link whose statistics is updated. + * @param start Time at which link switched its WifiPhyState to state. + * @param duration Duration of time that the link stayed in this state. + * @param state The state of the link from start to (start + duration). + */ + void AddLinkMeasurement(size_t linkId, Time start, Time duration, WifiPhyState state); + }; + + /** + * Default Constructor. StartTime is Seconds(0) and stopTime is Time::Max() + */ + WifiCoTraceHelper(); + + /** + * Construct a helper object measuring between two simulation time points [startTime, stopTime] + * + * @param startTime The measurement start time + * @param stopTime The measurement stop time + */ + WifiCoTraceHelper(Time startTime, Time stopTime); + + /** + * Enables trace collection for all nodes and WifiNetDevices in the specified NodeContainer. + * @param nodes The NodeContainer containing WifiNetDevices to which traces are to be connected. + */ + void Enable(NodeContainer nodes); + /** + * Enables trace collection for all nodes corresponding to the devices in the specified + * NetDeviceContainer. + * @param devices The NetDeviceContainer to which traces are to be connected. + */ + void Enable(NetDeviceContainer devices); + + /** + * Starts the collection of statistics from a specified start time. + * @param startTime The time to start collecting statistics. + */ + void Start(Time startTime); + + /** + * Stops the collection of statistics at a specified time. + * @param stopTime The time to stop collecting statistics. + */ + void Stop(Time stopTime); + + /** + * Resets the current statistics, clearing all links and their durations. It does not disconnect + * traced callbacks. It does not clear DeviceRecords. Only the statistics collected prior to + * invoking this method is cleared. + */ + void Reset(); + + /** + * Print measurement results on an output stream + * + * @param os The output stream to print to + * @param unit The unit of time in which the durations should be printed. Defaults to AUTO. + */ + void PrintStatistics(std::ostream& os, Time::Unit unit = Time::Unit::AUTO) const; + + /** + * Returns measurement results on each installed device. + * + * @return Statistics of each device traced by this helper. + */ + const std::vector& GetDeviceRecords() const; + + private: + uint32_t m_numDevices{0}; ///< Count the number of devices traced by this helper. + Time m_startTime; ///< Instant at which statistics collection should start. + Time m_stopTime{Time::Max()}; ///< Instant at which statistics collection should stop. + + std::vector m_deviceRecords; ///< Stores the collected statistics. + + /** + * A callback used to update statistics. + * + * @param idx Index of the device in m_deviceRecords vector. + * @param phyId Id of Phy whose state is switched. + * @param start Instant at which link switched its WifiPhyState to state. + * @param duration Duration of time the link stays in this state. + * @param state The state of the link from start to (start + duration). + */ + void NotifyWifiPhyState(std::size_t idx, + std::size_t phyId, + Time start, + Time duration, + WifiPhyState state); + + /** + * Compute overlapping time-duration between two intervals. It is a helper function used by + * NotifyWifiPhyState function. + * + * @param start1 Beginning of first interval + * @param stop1 End of first interval + * @param start2 Beginning of second interval + * @param stop2 End of second interval + * @return Return the time duration common between the two intervals. + */ + Time ComputeOverlappingDuration(Time start1, Time stop1, Time start2, Time stop2); + /** + * A helper function used by PrintStatistics method. It converts absolute statistics to + * percentages. + * + * @param linkStates Statistics of a link. + * @return Statistics in percentages. + */ + std::map ComputePercentage( + const std::map& linkStates) const; + + /** + * A helper function used by PrintStatistics method. It prints statistics of a link. + * + * @param os The output stream to print to + * @param linkStates Statistics of a link. + * @param unit The unit of time in which the durations should be printed. + * @return The output stream after IO operations. + */ + std::ostream& PrintLinkStates(std::ostream& os, + const std::map& linkStates, + Time::Unit unit) const; + + /** + * A helper function used by PrintLinkStates method to format the output. It pads each string at + * left with space characters such that the decimal points are at the same position. + * + * @param column A column of strings. Each string must contain a decimal point. + */ + void AlignDecimal(std::vector& column) const; + + /** + * A helper function used by PrintLinkStates method to format the output. It pads each string at + * right with space characters such that all strings have same width. + * + * @param column A column of strings. + */ + void AlignWidth(std::vector& column) const; +}; + +} // namespace ns3 + +#endif diff --git a/src/wifi/test/wifi-co-trace-helper-test.cc b/src/wifi/test/wifi-co-trace-helper-test.cc new file mode 100644 index 000000000..515d28605 --- /dev/null +++ b/src/wifi/test/wifi-co-trace-helper-test.cc @@ -0,0 +1,788 @@ +/* + * Copyright (c) 2024 Indraprastha Institute of Information Technology Delhi + * Copyright (c) 2009 University of Washington + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "ns3/boolean.h" +#include "ns3/double.h" +#include "ns3/eht-configuration.h" +#include "ns3/enum.h" +#include "ns3/he-phy.h" +#include "ns3/integer.h" +#include "ns3/mobility-helper.h" +#include "ns3/multi-model-spectrum-channel.h" +#include "ns3/names.h" +#include "ns3/node-list.h" +#include "ns3/object.h" +#include "ns3/packet-socket-client.h" +#include "ns3/packet-socket-helper.h" +#include "ns3/packet-socket-server.h" +#include "ns3/packet.h" +#include "ns3/pointer.h" +#include "ns3/rng-seed-manager.h" +#include "ns3/spectrum-wifi-helper.h" +#include "ns3/ssid.h" +#include "ns3/string.h" +#include "ns3/test.h" +#include "ns3/uinteger.h" +#include "ns3/wifi-co-trace-helper.h" +#include "ns3/wifi-helper.h" +#include "ns3/wifi-mac.h" +#include "ns3/wifi-mpdu.h" +#include "ns3/wifi-net-device.h" +#include "ns3/wifi-phy-state.h" +#include "ns3/wifi-psdu.h" +#include "ns3/yans-wifi-helper.h" + +using namespace ns3; +NS_LOG_COMPONENT_DEFINE("WifiCoTraceHelperTest"); + +/** + * @ingroup wifi-test + * @brief It's a base class with some utility methods for other test cases in this file. + */ +class WifiCoTraceHelperBaseTestCase : public TestCase +{ + public: + /** + * Constructor. + * + * @param testName Name of a test. + */ + WifiCoTraceHelperBaseTestCase(const std::string& testName) + : TestCase(testName) + { + } + + protected: + /** + * It gets the channel occupancy of a link on a node measured by WifiCoTraceHelper. + * + * @param nodeId Id of a node. + * @param linkId Id of link. + * @param coHelper Helper to obtain channel occupancy + * @return Statistics measured by WifiCoTraceHelper corresponding to nodeId,linkId. + */ + const std::map& GetChannelOccupancy(size_t nodeId, + size_t linkId, + WifiCoTraceHelper& coHelper); + + /** + * A helper function that sets tid-to-link mapping. + * + * @param mapping A string that configure tid-to-link mapping. + */ + void ConfigureTidToLinkMapping(const std::string& mapping); + + /** + * A helper function that installs PacketSocketServer on a node. + * + * @param node The node on which the server is installed. + * @param startAfter The server starts after a delay of "startAfter" duration. + * @param prot The PacketSocketAddress of the server uses "prot" as its protocol number. + */ + void InstallPacketSocketServer(Ptr node, Time startAfter, size_t prot); + + /** + * A helper function that installs PacketSocketClient application on a node. + * + * @param client The node on which the client is installed. + * @param server A node on which PacketSocketServer is installed and to whom the client sends + * requests. + * @param prot The protocol number used by both the client and the server. + * @param startAfter The client starts after a delay of "startAfter" duration. + * @param numPkts The client sends these many packets to server immediately. + * @param pktLen The length of each packet in bytes. + * @param tid The tid of each packet. + * @return A pointer to the installed PacketSocketClient application. + */ + Ptr InstallPacketSocketClient(Ptr client, + Ptr server, + size_t prot, + Time startAfter, + size_t numPkts, + size_t pktLen, + size_t tid); + + /** + * A helper function that schedules to send given number of packets from one node to another. + * + * @param from The node which sends the packets. + * @param to The node which receives the packets. + * @param startAfter The packets are scheduled to send after this much duration. + * @param numPkts The number of packets. + * @param pktLen The length of each packet in bytes. + * @param tid The optional tid of each packet. The default value is "0". + */ + void SchedulePackets(Ptr from, + Ptr to, + Time startAfter, + size_t numPkts, + size_t pktLen, + size_t tid); + + /** + * A helper function that disables frame aggregation. + */ + void DisableAggregation(); + + Time m_simulationStop{Seconds(5.0)}; ///< Instant at which simulation should stop. + NodeContainer m_nodes; ///< Container of all nodes instantiated in this test case. + NetDeviceContainer m_devices; ///< Container of all devices instantiated in this test case. + size_t protocol = + 1; ///< A unique Protocol used in each PacketSocketClient and PacketSocketServer pair. +}; + +const std::map& +WifiCoTraceHelperBaseTestCase::GetChannelOccupancy(size_t nodeId, + size_t linkId, + WifiCoTraceHelper& coHelper) +{ + const auto& devRecords = coHelper.GetDeviceRecords(); + auto senderRecord = std::find_if(devRecords.cbegin(), devRecords.cend(), [nodeId](auto& x) { + return x.m_nodeId == nodeId; + }); + + NS_ASSERT_MSG((senderRecord != devRecords.end()), "Expected statistics for nodeId: " << nodeId); + + auto stats = senderRecord->m_linkStateDurations.find(linkId); + auto endItr = senderRecord->m_linkStateDurations.end(); + NS_ASSERT_MSG((stats != endItr), + "Expected statistics at nodeId: " << nodeId << ", linkId: " << linkId); + + return stats->second; +} + +void +WifiCoTraceHelperBaseTestCase::ConfigureTidToLinkMapping(const std::string& mapping) +{ + for (size_t i = 0; i < m_devices.GetN(); ++i) + { + auto wifiDevice = DynamicCast(m_devices.Get(i)); + wifiDevice->GetMac()->GetEhtConfiguration()->SetAttribute( + "TidToLinkMappingNegSupport", + EnumValue(WifiTidToLinkMappingNegSupport::ANY_LINK_SET)); + + wifiDevice->GetMac()->GetEhtConfiguration()->SetAttribute("TidToLinkMappingUl", + StringValue(mapping)); + } +} + +/** + * Install Packet Socket Server on a node. A Packet Socket client generates an uplink flow and sends + * it to the server. + */ +void +WifiCoTraceHelperBaseTestCase::InstallPacketSocketServer(Ptr node, + Time startAfter, + size_t prot) +{ + Ptr device = DynamicCast(node->GetDevice(0)); + PacketSocketAddress srvAddr; + srvAddr.SetSingleDevice(device->GetIfIndex()); + srvAddr.SetProtocol(prot); + auto psServer = CreateObject(); + psServer->SetLocal(srvAddr); + node->AddApplication(psServer); + psServer->SetStartTime(startAfter); +} + +/** + * Install packet socket client that generates an uplink flow on a node. + */ +Ptr +WifiCoTraceHelperBaseTestCase::InstallPacketSocketClient(Ptr client, + Ptr server, + size_t prot, + Time startAfter, + size_t numPkts, + size_t pktLen, + size_t tid) +{ + NS_LOG_INFO("Start Flow on node:" << client->GetId() + << " at:" << Simulator::Now() + startAfter); + + Ptr staDevice = DynamicCast(client->GetDevice(0)); + PacketSocketAddress sockAddr; + sockAddr.SetSingleDevice(staDevice->GetIfIndex()); + sockAddr.SetPhysicalAddress(server->GetDevice(0)->GetAddress()); + sockAddr.SetProtocol(prot); + + auto clientApp = CreateObject(); + clientApp->SetAttribute("PacketSize", UintegerValue(pktLen)); + clientApp->SetAttribute("MaxPackets", UintegerValue(numPkts)); + clientApp->SetAttribute("Interval", TimeValue(Seconds(0))); // Send packets immediately + clientApp->SetAttribute("Priority", UintegerValue(tid)); + clientApp->SetRemote(sockAddr); + clientApp->SetStartTime(startAfter); + + staDevice->GetNode()->AddApplication(clientApp); + return clientApp; +} + +void +WifiCoTraceHelperBaseTestCase::SchedulePackets(Ptr from, + Ptr to, + Time startDelay, + size_t numPkts, + size_t pktLen, + size_t tid = 0) +{ + /* Install a PacketSocket server and client pair with a unique protocol on each invocation. */ + this->protocol++; + + InstallPacketSocketServer(to, startDelay, this->protocol); + InstallPacketSocketClient(from, to, this->protocol, startDelay, numPkts, pktLen, tid); +} + +void +WifiCoTraceHelperBaseTestCase::DisableAggregation() +{ + for (size_t i = 0; i < this->m_nodes.GetN(); i++) + { + for (uint32_t j = 0; j < this->m_nodes.Get(i)->GetNDevices(); j++) + { + auto device = DynamicCast(m_nodes.Get(i)->GetDevice(j)); + if (!device) + { + continue; + } + + device->GetMac()->SetAttribute("BE_MaxAmpduSize", UintegerValue(0)); + device->GetMac()->SetAttribute("BK_MaxAmpduSize", UintegerValue(0)); + device->GetMac()->SetAttribute("VO_MaxAmpduSize", UintegerValue(0)); + device->GetMac()->SetAttribute("VI_MaxAmpduSize", UintegerValue(0)); + + device->GetMac()->SetAttribute("BE_MaxAmsduSize", UintegerValue(0)); + device->GetMac()->SetAttribute("BK_MaxAmsduSize", UintegerValue(0)); + device->GetMac()->SetAttribute("VO_MaxAmsduSize", UintegerValue(0)); + device->GetMac()->SetAttribute("VI_MaxAmsduSize", UintegerValue(0)); + } + } +} + +/** + * @ingroup wifi-test + * @brief Send one packet from one WifiNetDevice to other. + * + * This test case sends a single packet from one wifi device to another, operating in Adhoc mode, + * and compares the TX duration measured by WifiCoTraceHelper with the analytically calculated + * value. + */ +class SendOnePacketTestCase : public WifiCoTraceHelperBaseTestCase +{ + public: + /** Constructor. */ + SendOnePacketTestCase(); + + private: + /** Executes test case and assertions. */ + void DoRun() override; + /** Setup test case's scenario. */ + void DoSetup() override; +}; + +SendOnePacketTestCase::SendOnePacketTestCase() + : WifiCoTraceHelperBaseTestCase( + "SendOnePacketTestCase: Send one packet from one WifiNetDevice to other.") +{ +} + +void +SendOnePacketTestCase::DoRun() +{ + size_t senderNodeId = 1; + size_t recNodeId = 0; + Ptr sender = m_nodes.Get(senderNodeId); + Ptr receiver = m_nodes.Get(recNodeId); + + // Send a packet to initiate one-time control frame exchanges in Adhoc mode. + // To avoid one-time control frames, we will not measure channel occupancy during transmission + // of this packet. + SchedulePackets(sender, receiver, Seconds(0.5), 1 /*num of packets*/, 1000 /*packet length*/); + + // Send a packet that requires only one symbol to transmit and measure its channel occupancy. + /** + * Size of frame in Bytes = 204 (payload) + 34 (Mac & Phy Headers) = 238 + * Frame Size in bits + Phy service and tail bits = 238*8 + 22 = 1926 + * Phy rate at MCS 11, 40 MHz, 800 ns GI, 1 spatial stream = 286.6 Mbps + * Symbol duration = 12.8 + 0.8 = 13.6 us + * Number of symbols = ceil(1926 / (286.6 * 13.6)) = 1 + */ + Time scheduleAt{Seconds(1.0)}; + size_t packetLen = 200; /*In Bytes*/ + SchedulePackets(sender, receiver, scheduleAt, 1 /*num of packets*/, packetLen); + WifiCoTraceHelper helper1{scheduleAt, scheduleAt + Seconds(0.1)}; + helper1.Enable(m_nodes); + + // Send a packet that requires two symbols to transmit and measure its channel occupancy. + /** + * If we replace payload size from 204 to 504 in the above calculation in comments, then number + * of symbols equals 2. + * Number of symbols = ceil(4326 / (286.6 * 13.6)) = 2 + */ + scheduleAt = Seconds(1.5); + packetLen = 500; /*In Bytes*/ + SchedulePackets(sender, receiver, scheduleAt, 1 /*num of packets*/, packetLen); + WifiCoTraceHelper helper2{scheduleAt, scheduleAt + Seconds(0.1)}; + helper2.Enable(m_nodes); + + // Send a packet that requires three symbols to transmit and measure its channel occupancy. + /** + * If we replace payload size from 204 to 1004 in the above calculation in comments, then number + * of symbols equals 3. + * Number of symbols = ceil(8326 / (286.6 * 13.6)) = 2 + */ + scheduleAt = Seconds(2.0); + packetLen = 1000; /*In Bytes*/ + SchedulePackets(sender, receiver, scheduleAt, 1 /*num of packets*/, packetLen); + WifiCoTraceHelper helper3{scheduleAt, scheduleAt + Seconds(0.1)}; + helper3.Enable(m_nodes); + + Simulator::Stop(Seconds(2.5)); + + Simulator::Run(); + Simulator::Destroy(); + + /** + * Data Packet + * =========== + * Preamble Duration = 48 us + * Symbol Duration = 12.8 + 0.8 (Guard Interval) = 13.6 us + * Tx Duration of a packet requiring 1 symbol = (1 * 13.6) + 48 = 61.6 us + * Tx Duration of a packet requiring 2 symbols = (2 * 13.6) + 48 = 75.2 us + * Tx Duration of a packet requiring 3 symbols = (3 * 13.6) + 48 = 88.8 us + */ + + auto map1 = GetChannelOccupancy(senderNodeId, 0 /*linkId*/, helper1); + NS_TEST_ASSERT_MSG_EQ(map1.at(WifiPhyState::TX), + NanoSeconds(61600), + "TX duration does not match"); + + auto map2 = GetChannelOccupancy(senderNodeId, 0 /*linkId*/, helper2); + NS_TEST_ASSERT_MSG_EQ(map2.at(WifiPhyState::TX), + NanoSeconds(75200), + "TX duration does not match"); + + auto map3 = GetChannelOccupancy(senderNodeId, 0 /*linkId*/, helper3); + NS_TEST_ASSERT_MSG_EQ(map3.at(WifiPhyState::TX), + NanoSeconds(88800), + "TX duration does not match"); + + /** + * Acknowledgement Packet + * ====================== + * Preamble Duration = 20 us + * Symbol Duration = 4 us + * Number of symbols = 2 + * Tx Duration of Ack packet = 2*4 + 20 = 28 us + */ + auto ackMap = GetChannelOccupancy(recNodeId, 0 /*linkId*/, helper1); + NS_TEST_ASSERT_MSG_EQ(ackMap.at(WifiPhyState::TX), + NanoSeconds(28000), + "TX duration does not match"); +} + +void +SendOnePacketTestCase::DoSetup() +{ + std::string mcs{"11"}; + + uint32_t nWifi = 2; + m_nodes.Create(nWifi); + + WifiMacHelper mac; + Ssid ssid = Ssid("ns-3-ssid"); + + WifiHelper wifi; + wifi.SetStandard(WIFI_STANDARD_80211be); + + // Create multiple spectrum channels + Ptr spectrumChannel5Ghz = CreateObject(); + + SpectrumWifiPhyHelper phy(1); + phy.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO); + phy.AddChannel(spectrumChannel5Ghz, WIFI_SPECTRUM_5_GHZ); + + // configure operating channel for each link + phy.Set(0, "ChannelSettings", StringValue("{0, 40, BAND_5GHZ, 0}")); + + uint8_t linkId = 0; + wifi.SetRemoteStationManager(linkId, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs" + mcs), + "ControlMode", + StringValue("OfdmRate24Mbps")); + + mac.SetType("ns3::AdhocWifiMac"); + m_devices = wifi.Install(phy, mac, m_nodes); + + MobilityHelper mobility; + Ptr positionAlloc = CreateObject(); + positionAlloc->Add(Vector(0.0, 0.0, 0.0)); + auto distance = 0.1; + positionAlloc->Add(Vector(distance, 0.0, 0.0)); + mobility.SetPositionAllocator(positionAlloc); + mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel"); + mobility.Install(m_nodes); + + /* Disable aggregation and set guard interval */ + DisableAggregation(); + int gi = 800; // Guard Interval in nano seconds + Config::Set("/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/HeConfiguration/GuardInterval", + TimeValue(NanoSeconds(gi))); + + PacketSocketHelper helper; + helper.Install(m_nodes); +} + +/** + * @ingroup wifi-test + * @brief Trace channel occupany on each link of MLDs + * + * This test case sends a packet on each link of a MLD and asserts that WifiCoTraceHelper measures + * TX duration correctly on every link. It uses tid-to-link mapping to schedule a packet to a + * specific link. + * + */ +class MLOTestCase : public WifiCoTraceHelperBaseTestCase +{ + public: + /** Constructor. */ + MLOTestCase(); + + private: + void DoRun() override; + void DoSetup() override; +}; + +MLOTestCase::MLOTestCase() + : WifiCoTraceHelperBaseTestCase( + "MLOTestCase: Track channel occupancy on multiple links of a multi-link device (MLD).") +{ +} + +void +MLOTestCase::DoRun() +{ + /* Configure tid-to-link mapping such that packets with different tids are sent on different + * links. */ + ConfigureTidToLinkMapping("0 0; 1,2,3,4,5,6,7 1"); + + size_t senderNodeId = 1; + size_t recNodeId = 0; + Ptr sender = m_nodes.Get(senderNodeId); + Ptr receiver = m_nodes.Get(recNodeId); + + SchedulePackets(sender, receiver, Seconds(0.5), 1, 1000, 0 /*tid*/); + SchedulePackets(sender, receiver, Seconds(0.5), 1, 1000, 3 /*tid*/); + + // Send a packet with tid '0' and measure channel occupancy. + Time scheduleAt = Seconds(1.05); + size_t tid = 0; + SchedulePackets(sender, receiver, scheduleAt, 1 /*num of packets*/, 1000 /*Bytes*/, tid); + WifiCoTraceHelper helper0{scheduleAt, scheduleAt + Seconds(0.01)}; + helper0.Enable(m_nodes); + + // Send a packet with tid '3' and measure channel occupancy. + scheduleAt = Seconds(2.0); + tid = 3; + SchedulePackets(sender, receiver, scheduleAt, 1 /*num of packets*/, 1000 /*Bytes*/, tid); + WifiCoTraceHelper helper1{scheduleAt, scheduleAt + Seconds(0.1)}; + helper1.Enable(m_nodes); + + Simulator::Stop(Seconds(3.5)); + + Simulator::Run(); + + std::cout << "## MLOTestCase: Tid 0 Packet ##\n"; + helper0.PrintStatistics(std::cout); + std::cout << "## MLOTestCase: Tid 3 Packet ##\n"; + helper1.PrintStatistics(std::cout); + + /* Assert TX time for each packet matches analytically compuated value of 88.8 us. Refer + * SendOnePacketTestCase above for the analytical calculation of TX time of 88.8 us.*/ + // Assert on link 0 + auto map0 = GetChannelOccupancy(senderNodeId, 0 /*linkId*/, helper0); + NS_TEST_ASSERT_MSG_EQ(map0.at(WifiPhyState::TX), + NanoSeconds(88800), + "TX duration does not match"); + + // Assert on link 1 + auto map1 = GetChannelOccupancy(senderNodeId, 1 /*linkId*/, helper1); + NS_TEST_ASSERT_MSG_EQ(map1.at(WifiPhyState::TX), + NanoSeconds(88800), + "TX duration does not match"); + + Simulator::Destroy(); +} + +void +MLOTestCase::DoSetup() +{ + // LogComponentEnable("WifiCoTraceHelper", LOG_LEVEL_INFO); + RngSeedManager::SetSeed(1); + + NodeContainer ap; + ap.Create(1); + + uint32_t nWifi = 1; + NodeContainer sta; + sta.Create(nWifi); + + m_nodes.Add(ap); + m_nodes.Add(sta); + + WifiMacHelper mac; + Ssid ssid = Ssid("ns-3-ssid"); + + WifiHelper wifi; + wifi.SetStandard(WIFI_STANDARD_80211be); + + // Create multiple spectrum channels + Ptr spectrumChannel5Ghz = CreateObject(); + Ptr spectrumChannel6Ghz = CreateObject(); + + // SpectrumWifiPhyHelper (3 links) + SpectrumWifiPhyHelper phy(2); + phy.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO); + phy.AddChannel(spectrumChannel5Ghz, WIFI_SPECTRUM_5_GHZ); + phy.AddChannel(spectrumChannel6Ghz, WIFI_SPECTRUM_6_GHZ); + + // configure operating channel for each link + phy.Set(0, "ChannelSettings", StringValue("{0, 40, BAND_5GHZ, 0}")); + phy.Set(1, "ChannelSettings", StringValue("{0, 40, BAND_6GHZ, 0}")); + + // configure rate manager for each link + uint8_t linkId = 0; + wifi.SetRemoteStationManager(linkId, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs11"), + "ControlMode", + StringValue("OfdmRate24Mbps")); + wifi.SetRemoteStationManager(1, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs11"), + "ControlMode", + StringValue("OfdmRate24Mbps")); + + mac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid)); + m_devices.Add(wifi.Install(phy, mac, ap)); + mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid), "ActiveProbing", BooleanValue(false)); + m_devices.Add(wifi.Install(phy, mac, sta)); + + MobilityHelper mobility; + Ptr positionAlloc = CreateObject(); + + positionAlloc->Add(Vector(0.0, 0.0, 0.0)); + auto distance = 1; + positionAlloc->Add(Vector(distance, 0.0, 0.0)); + mobility.SetPositionAllocator(positionAlloc); + + mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel"); + mobility.Install(m_nodes); + + /* Disable aggregation and set guard interval */ + DisableAggregation(); + int gi = 800; // Guard Interval in nano seconds + Config::Set("/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/HeConfiguration/GuardInterval", + TimeValue(NanoSeconds(gi))); + + PacketSocketHelper helper; + helper.Install(m_nodes); +} + +/** + * @ingroup wifi-test + * @brief LinkId of non-AP MLD changes after MLO setup. + * + * This test case configures one AP MLD with three links and one non-AP MLD with two links. Ngon-AP + * MLD swaps (i.e., renames) its link after MLO setup. It asserts that WifiCoTraceHelper should + * capture statistics of the renamed link. + */ +class LinkRenameTestCase : public WifiCoTraceHelperBaseTestCase +{ + public: + /** Constructor. */ + LinkRenameTestCase(); + + private: + void DoRun() override; + void DoSetup() override; +}; + +LinkRenameTestCase::LinkRenameTestCase() + : WifiCoTraceHelperBaseTestCase( + "LinkRenameTestCase: WifiCoTraceHelper should record statistics under new LinkId.") +{ +} + +void +LinkRenameTestCase::DoRun() +{ + // Adding nodes to wificohelper + WifiCoTraceHelper coHelper; + coHelper.Stop(m_simulationStop); + coHelper.Enable(m_nodes); + auto staNodeId = 1; + + SchedulePackets(m_nodes.Get(1), m_nodes.Get(0), Seconds(2.0), 1000, 1000); + Simulator::Stop(m_simulationStop); + + Simulator::Run(); + Simulator::Destroy(); + + std::cout << "## LinkRenameTestCase ##\n"; + coHelper.PrintStatistics(std::cout); + + auto staStatistics = coHelper.GetDeviceRecords().at(staNodeId).m_linkStateDurations; + + // Note that sta has only two phys. So, a linkId of '2' is created by renaming one of the + // existing links. + auto renamedLinkId = 2; + auto it = staStatistics.find(renamedLinkId); + NS_TEST_ASSERT_MSG_EQ((it != staStatistics.end()), + true, + "Link: " << renamedLinkId << " isn't present at non-AP MLD"); +} + +void +LinkRenameTestCase::DoSetup() +{ + // LogComponentEnable("WifiCoTraceHelper", LOG_LEVEL_DEBUG); + + m_simulationStop = Seconds(3); + + NodeContainer ap; + ap.Create(1); + + uint32_t nWifi = 1; + NodeContainer sta; + sta.Create(nWifi); + + m_nodes.Add(ap); + m_nodes.Add(sta); + + WifiMacHelper mac; + Ssid ssid = Ssid("ns-3-ssid"); + + // Create multiple spectrum channels + Ptr spectrumChannel2_4Ghz = + CreateObject(); + Ptr spectrumChannel5Ghz = CreateObject(); + + // SpectrumWifiPhyHelper (2 links) + SpectrumWifiPhyHelper nonApPhyHelper(2); + nonApPhyHelper.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO); + nonApPhyHelper.AddChannel(spectrumChannel5Ghz, WIFI_SPECTRUM_5_GHZ); + nonApPhyHelper.AddChannel(spectrumChannel5Ghz, WIFI_SPECTRUM_5_GHZ); + + // configure operating channel for each link + nonApPhyHelper.Set(0, "ChannelSettings", StringValue("{42, 80, BAND_5GHZ, 0}")); + nonApPhyHelper.Set(1, "ChannelSettings", StringValue("{0, 80, BAND_5GHZ, 0}")); + + nonApPhyHelper.Set("FixedPhyBand", BooleanValue(true)); + + WifiHelper nonApWifiHelper; + nonApWifiHelper.SetStandard(WIFI_STANDARD_80211be); + + // configure rate manager for each link + uint8_t firstLinkId = 0; + nonApWifiHelper.SetRemoteStationManager(firstLinkId, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs9"), + "ControlMode", + StringValue("EhtMcs9")); + nonApWifiHelper.SetRemoteStationManager(1, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs9"), + "ControlMode", + StringValue("EhtMcs9")); + + SpectrumWifiPhyHelper apPhyHelper(3); + apPhyHelper.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO); + apPhyHelper.AddChannel(spectrumChannel2_4Ghz, WIFI_SPECTRUM_2_4_GHZ); + apPhyHelper.AddChannel(spectrumChannel5Ghz, WIFI_SPECTRUM_5_GHZ); + apPhyHelper.AddChannel(spectrumChannel5Ghz, WIFI_SPECTRUM_5_GHZ); + + // configure operating channel for each link + apPhyHelper.Set(0, "ChannelSettings", StringValue("{6, 40, BAND_2_4GHZ, 0}")); + apPhyHelper.Set(1, "ChannelSettings", StringValue("{42, 80, BAND_5GHZ, 0}")); + apPhyHelper.Set(2, "ChannelSettings", StringValue("{0, 0, BAND_5GHZ, 0}")); + + apPhyHelper.Set("FixedPhyBand", BooleanValue(true)); + + WifiHelper apWifiHelper; + apWifiHelper.SetStandard(WIFI_STANDARD_80211be); + + apWifiHelper.SetRemoteStationManager(firstLinkId, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs9"), + "ControlMode", + StringValue("EhtMcs9")); + apWifiHelper.SetRemoteStationManager(1, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs9"), + "ControlMode", + StringValue("EhtMcs9")); + apWifiHelper.SetRemoteStationManager(2, + "ns3::ConstantRateWifiManager", + "DataMode", + StringValue("EhtMcs9"), + "ControlMode", + StringValue("EhtMcs9")); + + mac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid), "BeaconGeneration", BooleanValue(true)); + m_devices.Add(apWifiHelper.Install(apPhyHelper, mac, ap)); + + mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid), "ActiveProbing", BooleanValue(true)); + m_devices.Add(nonApWifiHelper.Install(nonApPhyHelper, mac, sta)); + + MobilityHelper mobility; + Ptr positionAlloc = CreateObject(); + + positionAlloc->Add(Vector(0.0, 0.0, 0.0)); + auto distance = 0.1; + positionAlloc->Add(Vector(distance, 0.0, 0.0)); + mobility.SetPositionAllocator(positionAlloc); + + mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel"); + mobility.Install(m_nodes); + + PacketSocketHelper helper; + helper.Install(m_nodes); +} + +/** + * @ingroup wifi-test + * @brief Wifi Channel Occupancy Helper Test Suite + */ +class WifiCoHelperTestSuite : public TestSuite +{ + public: + /** Constructor. */ + WifiCoHelperTestSuite(); +}; + +WifiCoHelperTestSuite::WifiCoHelperTestSuite() + : TestSuite("wifi-co-trace-helper", Type::UNIT) +{ + AddTestCase(new SendOnePacketTestCase, TestCase::Duration::QUICK); + AddTestCase(new MLOTestCase, TestCase::Duration::QUICK); + AddTestCase(new LinkRenameTestCase, TestCase::Duration::QUICK); +} + +/** + * @ingroup wifi-test + * WifiCoHelperTestSuite instance variable. + */ +static WifiCoHelperTestSuite g_WifiCoHelperTestSuite;