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