diff --git a/src/wifi/CMakeLists.txt b/src/wifi/CMakeLists.txt index 52e92894aa..bbe78e8424 100644 --- a/src/wifi/CMakeLists.txt +++ b/src/wifi/CMakeLists.txt @@ -373,6 +373,7 @@ build_lib( test/wifi-emlsr-test.cc test/wifi-error-rate-models-test.cc test/wifi-fils-frame-test.cc + test/wifi-gcr-test.cc test/wifi-he-info-elems-test.cc test/wifi-ie-fragment-test.cc test/wifi-mac-ofdma-test.cc diff --git a/src/wifi/model/rate-control/ideal-wifi-manager.h b/src/wifi/model/rate-control/ideal-wifi-manager.h index d0e61c03cf..57a2e4acc0 100644 --- a/src/wifi/model/rate-control/ideal-wifi-manager.h +++ b/src/wifi/model/rate-control/ideal-wifi-manager.h @@ -45,7 +45,7 @@ class IdealWifiManager : public WifiRemoteStationManager void SetupPhy(const Ptr phy) override; - private: + protected: void DoInitialize() override; WifiRemoteStation* DoCreateStation() const override; void DoReportRxOk(WifiRemoteStation* station, double rxSnr, WifiMode txMode) override; @@ -73,6 +73,7 @@ class IdealWifiManager : public WifiRemoteStationManager WifiTxVector DoGetDataTxVector(WifiRemoteStation* station, MHz_u allowedWidth) override; WifiTxVector DoGetRtsTxVector(WifiRemoteStation* station) override; + private: /** * Reset the station, invoked if the maximum amount of retries has failed. * diff --git a/src/wifi/test/wifi-gcr-test.cc b/src/wifi/test/wifi-gcr-test.cc new file mode 100644 index 0000000000..a18c355abf --- /dev/null +++ b/src/wifi/test/wifi-gcr-test.cc @@ -0,0 +1,1749 @@ +/* + * Copyright (c) 2023 DERONNE SOFTWARE ENGINEERING + * + * SPDX-License-Identifier: GPL-2.0-only + * + * Author: Sébastien Deronne + */ + +#include "wifi-gcr-test.h" + +#include "ns3/ampdu-subframe-header.h" +#include "ns3/attribute-container.h" +#include "ns3/boolean.h" +#include "ns3/config.h" +#include "ns3/ctrl-headers.h" +#include "ns3/he-configuration.h" +#include "ns3/ht-configuration.h" +#include "ns3/ideal-wifi-manager.h" +#include "ns3/log.h" +#include "ns3/mac48-address.h" +#include "ns3/mgt-action-headers.h" +#include "ns3/mgt-headers.h" +#include "ns3/mobility-helper.h" +#include "ns3/multi-model-spectrum-channel.h" +#include "ns3/node-list.h" +#include "ns3/packet-socket-client.h" +#include "ns3/packet-socket-helper.h" +#include "ns3/packet-socket-server.h" +#include "ns3/pointer.h" +#include "ns3/qos-txop.h" +#include "ns3/rng-seed-manager.h" +#include "ns3/rr-multi-user-scheduler.h" +#include "ns3/simulator.h" +#include "ns3/spectrum-wifi-helper.h" +#include "ns3/spectrum-wifi-phy.h" +#include "ns3/string.h" +#include "ns3/vht-configuration.h" +#include "ns3/wifi-net-device.h" +#include "ns3/wifi-ppdu.h" +#include "ns3/wifi-psdu.h" +#include "ns3/yans-wifi-helper.h" +#include "ns3/yans-wifi-phy.h" + +#include +#include +#include +#include +#include +#include + +using namespace ns3; + +namespace +{ + +/** + * Get the number of GCR STAs. + * + * @param stas information about all STAs of the test + * @return the number of GCR STAs + */ +std::size_t +GetNumGcrStas(const std::vector& stas) +{ + return std::count_if(stas.cbegin(), stas.cend(), [](const auto& staInfo) { + return (staInfo.gcrCapable); + }); +} + +/** + * Get the number of non-GCR STAs. + * + * @param stas information about all STAs of the test + * @return the number of non-GCR STAs + */ +std::size_t +GetNumNonGcrStas(const std::vector& stas) +{ + return stas.size() - GetNumGcrStas(stas); +} + +/** + * Get the number of non-HT STAs. + * + * @param stas information about all STAs of the test + * @return the number of non-HT STAs + */ +std::size_t +GetNumNonHtStas(const std::vector& stas) +{ + return std::count_if(stas.cbegin(), stas.cend(), [](const auto& staInfo) { + return staInfo.standard < WIFI_STANDARD_80211n; + }); +} + +/** + * Lambda to print stations information. + */ +auto printStasInfo = [](const std::vector& v) { + std::stringstream ss; + std::size_t index = 0; + ss << "{"; + for (const auto& staInfo : v) + { + ss << "STA" << ++index << ": GCRcapable=" << staInfo.gcrCapable + << " standard=" << staInfo.standard << " maxBw=" << staInfo.maxChannelWidth + << " maxNss=" << +staInfo.maxNumStreams << " minGi=" << staInfo.minGi << "; "; + } + ss << "}"; + return ss.str(); +}; + +/** + * Get the node ID from the context string. + * + * @param context the context string + * @return the corresponding node ID + */ +uint32_t +ConvertContextToNodeId(const std::string& context) +{ + auto sub = context.substr(10); + auto pos = sub.find("/Device"); + return std::stoi(sub.substr(0, pos)); +} + +/** + * Get the maximum number of groupcast MPDUs that can be in flight. + * + * @param maxNumMpdus the configured maximum number of MPDUs + * @param stas information about all STAs of the test + * @return the maximum number of groupcast MPDUs that can be in flight + */ +uint16_t +GetGcrMaxNumMpdus(uint16_t maxNumMpdus, const std::vector& stas) +{ + uint16_t limit = 1024; + for (const auto& staInfo : stas) + { + if (staInfo.standard < WIFI_STANDARD_80211ax) + { + limit = 64; + break; + } + if (staInfo.standard < WIFI_STANDARD_80211be) + { + limit = 256; + } + } + return std::min(limit, maxNumMpdus); +} + +constexpr uint16_t MULTICAST_PROTOCOL{1}; ///< protocol to create socket for multicast +constexpr uint16_t UNICAST_PROTOCOL{2}; ///< protocol to create socket for unicast + +constexpr uint32_t maxRtsCtsThreshold{4692480}; ///< maximum value for RTS/CTS threshold + +constexpr bool GCR_CAPABLE_STA{true}; ///< STA that is GCR capable +constexpr bool GCR_INCAPABLE_STA{false}; ///< STA that is not GCR capable + +} // namespace + +/** + * Extended IdealWifiManager class for the purpose of the tests. + */ +class IdealWifiManagerForGcrTest : public IdealWifiManager +{ + public: + /** + * @brief Get the type ID. + * @return the object TypeId + */ + static TypeId GetTypeId() + { + static TypeId tid = TypeId("ns3::IdealWifiManagerForGcrTest") + .SetParent() + .SetGroupName("Wifi") + .AddConstructor(); + return tid; + } + + void DoReportAmpduTxStatus(WifiRemoteStation* station, + uint16_t nSuccessfulMpdus, + uint16_t nFailedMpdus, + double rxSnr, + double dataSnr, + MHz_u dataChannelWidth, + uint8_t dataNss) override + { + m_blockAckSenders.insert(station->m_state->m_address); + IdealWifiManager::DoReportAmpduTxStatus(station, + nSuccessfulMpdus, + nFailedMpdus, + rxSnr, + dataSnr, + dataChannelWidth, + dataNss); + } + + GcrManager::GcrMembers m_blockAckSenders; ///< hold set of BACK senders that have passed + ///< success/failure infos to RSM +}; + +NS_OBJECT_ENSURE_REGISTERED(IdealWifiManagerForGcrTest); + +NS_LOG_COMPONENT_DEFINE("WifiGcrTest"); + +GcrTestBase::GcrTestBase(const std::string& testName, const GcrParameters& params) + : TestCase(testName), + m_testName{testName}, + m_params{params}, + m_expectGcrUsed{(GetNumGcrStas(params.stas) > 0)}, + m_expectedMaxNumMpdusInPsdu( + m_expectGcrUsed ? GetGcrMaxNumMpdus(m_params.maxNumMpdusInPsdu, params.stas) : 1U), + m_packets{0}, + m_nTxApRts{0}, + m_nTxApCts{0}, + m_totalTx{0}, + m_nTxGroupcastInCurrentTxop{0}, + m_nTxRtsInCurrentTxop{0}, + m_nTxCtsInCurrentTxop{0}, + m_nTxAddbaReq{0}, + m_nTxAddbaResp{0}, + m_nTxDelba{0}, + m_nTxGcrAddbaReq{0}, + m_nTxGcrAddbaResp{0}, + m_nTxGcrDelba{0} +{ + m_params.maxNumMpdusInPsdu = m_expectGcrUsed ? params.maxNumMpdusInPsdu : 1U; +} + +void +GcrTestBase::PacketGenerated(std::string context, Ptr p, const Address& addr) +{ + m_packets++; + if ((m_packets % m_expectedMaxNumMpdusInPsdu) == 0) + { + m_groupcastClient->SetAttribute("Interval", TimeValue(MilliSeconds(10))); + } + else + { + m_groupcastClient->SetAttribute("Interval", TimeValue(Seconds(0))); + } +} + +void +GcrTestBase::Transmit(std::string context, + WifiConstPsduMap psduMap, + WifiTxVector txVector, + double txPowerW) +{ + auto psdu = psduMap.cbegin()->second; + auto mpdu = *psdu->begin(); + auto addr1 = mpdu->GetHeader().GetAddr1(); + const auto nodeId = ConvertContextToNodeId(context); + if (addr1.IsGroup() && !addr1.IsBroadcast() && mpdu->GetHeader().IsQosData()) + { + const auto expectedChannelWidth = + std::min_element(m_params.stas.cbegin(), + m_params.stas.cend(), + [](const auto& sta1, const auto& sta2) { + return sta1.maxChannelWidth < sta2.maxChannelWidth; + }) + ->maxChannelWidth; + NS_TEST_EXPECT_MSG_EQ(txVector.GetChannelWidth(), + expectedChannelWidth, + "Incorrect channel width for groupcast frame"); + const auto expectedNss = + std::min_element(m_params.stas.cbegin(), + m_params.stas.cend(), + [](const auto& sta1, const auto& sta2) { + return sta1.maxNumStreams < sta2.maxNumStreams; + }) + ->maxNumStreams; + const auto expectedGi = std::max_element(m_params.stas.cbegin(), + m_params.stas.cend(), + [](const auto& sta1, const auto& sta2) { + return sta1.minGi < sta2.minGi; + }) + ->minGi; + NS_TEST_EXPECT_MSG_EQ(+txVector.GetNss(), + +expectedNss, + "Incorrect number of spatial streams for groupcast frame"); + NS_TEST_EXPECT_MSG_EQ(txVector.GetGuardInterval(), + expectedGi, + "Incorrect guard interval duration for groupcast frame"); + Mac48Address expectedGroupAddress{"01:00:5e:40:64:01"}; + Mac48Address groupConcealmentAddress{"01:0F:AC:47:43:52"}; + const auto expectConcealmentUsed = + m_expectGcrUsed && + (GetNumNonGcrStas(m_params.stas) == 0 || mpdu->GetHeader().IsRetry()); + std::vector addressedStas{}; + if (!expectConcealmentUsed) + { + std::copy_if(m_params.stas.cbegin(), + m_params.stas.cend(), + std::back_inserter(addressedStas), + [](const auto& sta) { return !sta.gcrCapable; }); + } + else + { + std::copy_if(m_params.stas.cbegin(), + m_params.stas.cend(), + std::back_inserter(addressedStas), + [](const auto& sta) { return sta.gcrCapable; }); + } + NS_ASSERT(!addressedStas.empty()); + const auto minStandard = std::min_element(addressedStas.cbegin(), + addressedStas.cend(), + [](const auto& sta1, const auto& sta2) { + return sta1.standard < sta2.standard; + }) + ->standard; + const auto expectedModulationClass = GetModulationClassForStandard(minStandard); + NS_TEST_EXPECT_MSG_EQ(txVector.GetModulationClass(), + expectedModulationClass, + "Incorrect modulation class for groupcast frame"); + NS_TEST_EXPECT_MSG_EQ( + addr1, + (expectConcealmentUsed ? groupConcealmentAddress : expectedGroupAddress), + "Unexpected address1"); + NS_TEST_EXPECT_MSG_EQ(mpdu->GetHeader().IsQosAmsdu(), + expectConcealmentUsed, + "MSDU aggregation should " << (expectConcealmentUsed ? "" : "not ") + << "be used"); + if (mpdu->GetHeader().IsQosAmsdu()) + { + const auto numAmsduSubframes = std::distance(mpdu->begin(), mpdu->end()); + NS_TEST_EXPECT_MSG_EQ( + numAmsduSubframes, + 1, + "Only one A-MSDU subframe should be used in concealed group addressed frames"); + NS_TEST_EXPECT_MSG_EQ(mpdu->begin()->second.GetDestinationAddr(), + expectedGroupAddress, + "Unexpected DA field in A-MSDU subframe"); + } + m_totalTx++; + if (const auto it = m_params.mpdusToCorruptPerPsdu.find(m_totalTx); + it != m_params.mpdusToCorruptPerPsdu.cend()) + { + std::map> uidListPerSta{}; + uint8_t numStas = m_params.stas.size(); + for (uint8_t i = 0; i < numStas; ++i) + { + uidListPerSta.insert({i, {}}); + } + for (std::size_t i = 0; i < psdu->GetNMpdus(); ++i) + { + for (uint8_t staId = 0; staId < numStas; ++staId) + { + const auto& corruptedMpdusForSta = + (it->second.count(0) != 0) + ? it->second.at(0) + : ((it->second.count(staId + 1) != 0) ? it->second.at(staId + 1) + : std::set{}); + if (std::find(corruptedMpdusForSta.cbegin(), + corruptedMpdusForSta.cend(), + i + 1) != std::end(corruptedMpdusForSta)) + { + NS_LOG_INFO("STA " << staId + 1 << ": corrupted MPDU #" << i + 1 << " (seq=" + << psdu->GetHeader(i).GetSequenceNumber() << ")" + << " for frame #" << +m_totalTx); + uidListPerSta.at(staId).push_back(psdu->GetAmpduSubframe(i)->GetUid()); + } + else + { + NS_LOG_INFO("STA " << staId + 1 << ": uncorrupted MPDU #" << i + 1 + << " (seq=" << psdu->GetHeader(i).GetSequenceNumber() + << ")" + << " for frame #" << +m_totalTx); + } + } + } + for (uint8_t staId = 0; staId < numStas; ++staId) + { + m_errorModels.at(staId)->SetList(uidListPerSta.at(staId)); + } + } + else + { + NS_LOG_INFO("Do not corrupt frame #" << +m_totalTx); + for (auto& errorModel : m_errorModels) + { + errorModel->SetList({}); + } + } + } + else if (mpdu->GetHeader().IsRts()) + { + const auto isGroupcast = (m_params.numUnicastPackets == 0) || + ((m_params.startUnicast < m_params.startGroupcast) && + (Simulator::Now() > m_params.startGroupcast)) || + ((m_params.startGroupcast < m_params.startUnicast) && + (Simulator::Now() < m_params.startUnicast)); + if (isGroupcast) + { + NS_TEST_EXPECT_MSG_EQ(nodeId, 0, "STAs are not expected to send RTS frames"); + NS_LOG_INFO("AP: start protection and initiate RTS-CTS"); + NS_TEST_EXPECT_MSG_EQ(mpdu->GetHeader().GetAddr2(), + m_apWifiMac->GetAddress(), + "Incorrect Address2 set for RTS frame"); + const auto it = std::find_if(m_stasWifiMac.cbegin(), + m_stasWifiMac.cend(), + [addr = mpdu->GetHeader().GetAddr1()](const auto& mac) { + return addr == mac->GetAddress(); + }); + NS_TEST_EXPECT_MSG_EQ((it != m_stasWifiMac.cend()), + true, + "Incorrect Address1 set for RTS frame"); + m_nTxApRts++; + if (m_params.rtsFramesToCorrupt.count(m_nTxApRts)) + { + NS_LOG_INFO("Corrupt RTS frame #" << +m_nTxApRts); + const auto uid = mpdu->GetPacket()->GetUid(); + for (auto& errorModel : m_errorModels) + { + errorModel->SetList({uid}); + } + } + else + { + NS_LOG_INFO("Do not corrupt RTS frame #" << +m_nTxApRts); + for (auto& errorModel : m_errorModels) + { + errorModel->SetList({}); + } + } + } + } + else if (mpdu->GetHeader().IsCts()) + { + const auto isGroupcast = (m_params.numUnicastPackets == 0) || + ((m_params.startUnicast < m_params.startGroupcast) && + (Simulator::Now() > m_params.startGroupcast)) || + ((m_params.startGroupcast < m_params.startUnicast) && + (Simulator::Now() < m_params.startUnicast)); + if (isGroupcast) + { + if (nodeId == 0) + { + NS_LOG_INFO("AP: start protection and initiate CTS-to-self"); + NS_TEST_EXPECT_MSG_EQ(mpdu->GetHeader().GetAddr1(), + m_apWifiMac->GetAddress(), + "Incorrect Address1 set for CTS-to-self frame"); + m_nTxApCts++; + } + else + { + const auto staId = nodeId - 1; + NS_LOG_INFO("STA" << staId + 1 << ": send CTS response"); + NS_TEST_EXPECT_MSG_EQ(mpdu->GetHeader().GetAddr1(), + m_apWifiMac->GetAddress(), + "Incorrect Address1 set for CTS frame"); + m_txCtsPerSta.at(staId)++; + if (const auto it = m_params.ctsFramesToCorrupt.find(m_txCtsPerSta.at(staId)); + it != m_params.ctsFramesToCorrupt.cend()) + { + NS_LOG_INFO("Corrupt CTS frame #" << +m_txCtsPerSta.at(staId)); + const auto uid = mpdu->GetPacket()->GetUid(); + m_apErrorModel->SetList({uid}); + } + else + { + NS_LOG_INFO("Do not corrupt CTS frame #" << +m_txCtsPerSta.at(staId)); + m_apErrorModel->SetList({}); + } + } + } + } + else if (mpdu->GetHeader().IsAction()) + { + WifiActionHeader actionHdr; + Ptr packet = mpdu->GetPacket()->Copy(); + packet->RemoveHeader(actionHdr); + auto [category, action] = WifiActionHeader::Peek(mpdu->GetPacket()); + if (category == WifiActionHeader::BLOCK_ACK) + { + Mac48Address expectedGroupAddress{"01:00:5e:40:64:01"}; + if (action.blockAck == WifiActionHeader::BLOCK_ACK_ADDBA_REQUEST) + { + MgtAddBaRequestHeader reqHdr; + packet->RemoveHeader(reqHdr); + const auto isGcr = reqHdr.GetGcrGroupAddress().has_value(); + NS_LOG_INFO("AP: send " << (isGcr ? "GCR " : "") << "ADDBA request"); + const auto expectedGcr = + m_expectGcrUsed && ((m_params.numUnicastPackets == 0) || + ((m_params.startUnicast < m_params.startGroupcast) && + (Simulator::Now() > m_params.startGroupcast)) || + ((m_params.startGroupcast < m_params.startUnicast) && + (Simulator::Now() < m_params.startUnicast))); + NS_TEST_EXPECT_MSG_EQ(isGcr, + expectedGcr, + "GCR address should " + << (expectedGcr ? "" : "not ") + << "be set in ADDBA request sent from AP"); + if (isGcr) + { + m_nTxGcrAddbaReq++; + NS_TEST_EXPECT_MSG_EQ(reqHdr.GetGcrGroupAddress().value(), + expectedGroupAddress, + "Incorrect GCR address in ADDBA request sent from AP"); + if (m_params.addbaReqsToCorrupt.count(m_nTxGcrAddbaReq)) + { + NS_LOG_INFO("Corrupt ADDBA request #" << +m_nTxGcrAddbaReq); + const auto uid = mpdu->GetPacket()->GetUid(); + for (auto& errorModel : m_errorModels) + { + errorModel->SetList({uid}); + } + } + else + { + NS_LOG_INFO("Do not corrupt ADDBA request #" << +m_nTxGcrAddbaReq); + for (auto& errorModel : m_errorModels) + { + errorModel->SetList({}); + } + } + } + else + { + m_nTxAddbaReq++; + } + } + else if (action.blockAck == WifiActionHeader::BLOCK_ACK_ADDBA_RESPONSE) + { + MgtAddBaResponseHeader respHdr; + packet->RemoveHeader(respHdr); + const auto isGcr = respHdr.GetGcrGroupAddress().has_value(); + NS_LOG_INFO("STA" << nodeId << ": send " << (isGcr ? "GCR " : "") + << "ADDBA response"); + const auto expectedGcr = + m_expectGcrUsed && ((m_params.numUnicastPackets == 0) || + ((m_params.startUnicast < m_params.startGroupcast) && + (Simulator::Now() > m_params.startGroupcast)) || + ((m_params.startGroupcast < m_params.startUnicast) && + (Simulator::Now() < m_params.startUnicast))); + NS_TEST_EXPECT_MSG_EQ(isGcr, + expectedGcr, + "GCR address should " + << (expectedGcr ? "" : "not ") + << "be set in ADDBA response sent from STA " << nodeId); + if (isGcr) + { + m_nTxGcrAddbaResp++; + NS_TEST_EXPECT_MSG_EQ(respHdr.GetGcrGroupAddress().value(), + expectedGroupAddress, + "Incorrect GCR address in ADDBA request sent from STA " + << nodeId); + if (const auto it = m_params.addbaRespsToCorrupt.find(m_nTxGcrAddbaResp); + it != m_params.addbaRespsToCorrupt.cend()) + { + NS_LOG_INFO("Corrupt ADDBA response #" << +m_nTxGcrAddbaResp); + const auto uid = mpdu->GetPacket()->GetUid(); + m_apErrorModel->SetList({uid}); + } + else + { + NS_LOG_INFO("Do not corrupt ADDBA response #" << +m_nTxGcrAddbaResp); + m_apErrorModel->SetList({}); + } + } + else + { + m_nTxAddbaResp++; + } + } + else if (action.blockAck == WifiActionHeader::BLOCK_ACK_DELBA) + { + MgtDelBaHeader delbaHdr; + packet->RemoveHeader(delbaHdr); + const auto isGcr = delbaHdr.GetGcrGroupAddress().has_value(); + NS_LOG_INFO("AP: send " << (isGcr ? "GCR " : "") << "DELBA frame"); + const auto expectedGcr = + m_expectGcrUsed && ((m_params.numUnicastPackets == 0) || + ((m_params.startUnicast < m_params.startGroupcast) && + (Simulator::Now() > m_params.startGroupcast)) || + ((m_params.startGroupcast < m_params.startUnicast) && + (Simulator::Now() < m_params.startUnicast))); + NS_TEST_EXPECT_MSG_EQ(isGcr, + expectedGcr, + "GCR address should " + << (expectedGcr ? "" : "not ") + << "be set in DELBA frame sent from AP"); + if (isGcr) + { + m_nTxGcrDelba++; + } + else + { + m_nTxDelba++; + } + } + } + } +} + +bool +GcrTestBase::IsUsingAmpduOrSmpdu() const +{ + return ((m_params.maxNumMpdusInPsdu > 1) || + std::none_of(m_params.stas.cbegin(), m_params.stas.cend(), [](const auto& staInfo) { + return (staInfo.standard < WIFI_STANDARD_80211ac); + })); +} + +void +GcrTestBase::PhyRx(std::string context, + Ptr p, + double snr, + WifiMode mode, + WifiPreamble preamble) +{ + const auto packetSize = p->GetSize(); + if (packetSize < m_params.packetSize) + { + // ignore small packets (ACKs, ...) + return; + } + Ptr packet = p->Copy(); + if (IsUsingAmpduOrSmpdu()) + { + AmpduSubframeHeader ampduHdr; + packet->RemoveHeader(ampduHdr); + } + WifiMacHeader hdr; + packet->PeekHeader(hdr); + if (!hdr.IsData() || !hdr.GetAddr1().IsGroup()) + { + // ignore non-data frames and unicast data frames + return; + } + const auto staId = ConvertContextToNodeId(context) - 1; + NS_ASSERT(staId <= m_params.stas.size()); + m_phyRxPerSta.at(staId)++; +} + +void +GcrTestBase::NotifyTxopTerminated(Time startTime, Time duration, uint8_t linkId) +{ + NS_LOG_INFO("AP: terminated TXOP"); + NS_TEST_EXPECT_MSG_EQ((m_nTxGroupcastInCurrentTxop <= 1), + true, + "An MPDU and a retransmission of the same MPDU shall not be transmitted " + "within the same GCR TXOP"); + NS_TEST_EXPECT_MSG_EQ((m_nTxRtsInCurrentTxop + m_nTxCtsInCurrentTxop <= 1), + true, + "No more than one protection frame exchange per GCR TXOP"); + m_nTxGroupcastInCurrentTxop = 0; + m_nTxRtsInCurrentTxop = 0; + m_nTxCtsInCurrentTxop = 0; +} + +void +GcrTestBase::CheckResults() +{ + NS_LOG_FUNCTION(this); + + const auto expectedNumRts = + (m_expectGcrUsed && (m_params.gcrProtectionMode == GroupcastProtectionMode::RTS_CTS) && + (m_params.rtsThreshold < (m_params.packetSize * m_params.maxNumMpdusInPsdu))) + ? m_totalTx + m_params.rtsFramesToCorrupt.size() + m_params.ctsFramesToCorrupt.size() + : 0U; + NS_TEST_EXPECT_MSG_EQ(+m_nTxApRts, +expectedNumRts, "Unexpected number of RTS frames"); + + const auto expectedNumCts = + (m_expectGcrUsed && (m_params.gcrProtectionMode == GroupcastProtectionMode::RTS_CTS) && + (m_params.rtsThreshold < (m_params.packetSize * m_params.maxNumMpdusInPsdu))) + ? m_totalTx + m_params.ctsFramesToCorrupt.size() + : 0U; + const auto totalNumCts = std::accumulate(m_txCtsPerSta.cbegin(), m_txCtsPerSta.cend(), 0U); + NS_TEST_EXPECT_MSG_EQ(totalNumCts, expectedNumCts, "Unexpected number of CTS frames"); + + const auto expectedNumCtsToSelf = + (m_expectGcrUsed && (m_params.gcrProtectionMode == GroupcastProtectionMode::CTS_TO_SELF)) + ? m_totalTx + : 0U; + NS_TEST_EXPECT_MSG_EQ(+m_nTxApCts, + +expectedNumCtsToSelf, + "Unexpected number of CTS-to-self frames"); + + uint8_t numStas = m_params.stas.size(); + for (uint8_t i = 0; i < numStas; ++i) + { + NS_TEST_EXPECT_MSG_EQ(m_rxUnicastPerSta.at(i), + m_params.numUnicastPackets, + "Unexpected number of received unicast packets for STA " << i + 1); + } + + const auto htCapableStas = + std::count_if(m_params.stas.cbegin(), m_params.stas.cend(), [](const auto& staInfo) { + return (staInfo.standard >= WIFI_STANDARD_80211n); + }); + if (m_params.numUnicastPackets > 0) + { + NS_TEST_EXPECT_MSG_EQ(+m_nTxAddbaReq, + +htCapableStas, + "Incorrect number of transmitted ADDBA requests"); + NS_TEST_EXPECT_MSG_EQ(+m_nTxAddbaResp, + +htCapableStas, + "Incorrect number of transmitted ADDBA responses"); + NS_TEST_EXPECT_MSG_EQ(+m_nTxDelba, + ((m_params.baInactivityTimeout > 0 && m_params.numUnicastPackets > 1) + ? +htCapableStas + : 0), + "Incorrect number of transmitted DELBA frames"); + } + + const auto gcrCapableStas = + std::count_if(m_params.stas.cbegin(), m_params.stas.cend(), [](const auto& staInfo) { + return staInfo.gcrCapable; + }); + if (m_params.numGroupcastPackets > 0 && (m_params.maxNumMpdusInPsdu > 1)) + { + NS_TEST_EXPECT_MSG_EQ(+m_nTxGcrAddbaReq, + gcrCapableStas + m_params.addbaReqsToCorrupt.size(), + "Incorrect number of transmitted GCR ADDBA requests"); + NS_TEST_EXPECT_MSG_EQ(+m_nTxGcrAddbaResp, + gcrCapableStas + m_params.addbaRespsToCorrupt.size(), + "Incorrect number of transmitted GCR ADDBA responses"); + NS_TEST_EXPECT_MSG_EQ(+m_nTxGcrDelba, + ((m_params.baInactivityTimeout > 0) ? +gcrCapableStas : 0), + "Incorrect number of transmitted GCR DELBA frames"); + } + else + { + NS_TEST_EXPECT_MSG_EQ(+m_nTxGcrAddbaReq, 0, "Unexpected GCR ADDBA requests"); + NS_TEST_EXPECT_MSG_EQ(+m_nTxGcrAddbaResp, 0, "Unexpected GCR ADDBA responses"); + NS_TEST_EXPECT_MSG_EQ(+m_nTxGcrDelba, 0, "Unexpected GCR DELBA frames"); + } +} + +void +GcrTestBase::DoSetup() +{ + NS_LOG_FUNCTION(this << m_testName); + + // WifiHelper::EnableLogComponents(); + // LogComponentEnable("WifiGcrTest", LOG_LEVEL_ALL); + + RngSeedManager::SetSeed(1); + RngSeedManager::SetRun(70); + int64_t streamNumber = 100; + + Config::SetDefault("ns3::WifiMacQueue::MaxDelay", TimeValue(m_params.maxLifetime)); + const auto maxPacketsInQueue = std::max(m_params.numGroupcastPackets + 1, 500); + Config::SetDefault("ns3::WifiMacQueue::MaxSize", + StringValue(std::to_string(maxPacketsInQueue) + "p")); + + const auto maxChannelWidth = + std::max_element(m_params.stas.cbegin(), + m_params.stas.cend(), + [](const auto& lhs, const auto& rhs) { + return lhs.maxChannelWidth < rhs.maxChannelWidth; + }) + ->maxChannelWidth; + const auto maxNss = std::max_element(m_params.stas.cbegin(), + m_params.stas.cend(), + [](const auto& lhs, const auto& rhs) { + return lhs.maxNumStreams < rhs.maxNumStreams; + }) + ->maxNumStreams; + + uint8_t numStas = m_params.stas.size(); + NodeContainer wifiApNode(1); + NodeContainer wifiStaNodes(numStas); + + WifiHelper wifi; + wifi.SetStandard(WIFI_STANDARD_80211be); + wifi.SetRemoteStationManager("ns3::IdealWifiManagerForGcrTest", + "RtsCtsThreshold", + UintegerValue(m_params.rtsThreshold), + "NonUnicastMode", + (GetNumNonHtStas(m_params.stas) == 0) + ? StringValue("HtMcs0") + : StringValue("OfdmRate6Mbps")); + + wifi.ConfigHtOptions("ShortGuardIntervalSupported", BooleanValue(true)); + wifi.ConfigHeOptions("GuardInterval", TimeValue(NanoSeconds(800))); + + WifiMacHelper apMacHelper; + apMacHelper.SetType("ns3::ApWifiMac", + "Ssid", + SsidValue(Ssid("ns-3-ssid")), + "BeaconGeneration", + BooleanValue(true), + "RobustAVStreamingSupported", + BooleanValue(true)); + ConfigureGcrManager(apMacHelper); + + WifiMacHelper staMacHelper; + staMacHelper.SetType("ns3::StaWifiMac", + "Ssid", + SsidValue(Ssid("ns-3-ssid")), + "ActiveProbing", + BooleanValue(false), + "QosSupported", + BooleanValue(true)); + ConfigureGcrManager(staMacHelper); + + NetDeviceContainer apDevice; + NetDeviceContainer staDevices; + + const auto differentChannelWidths = + std::any_of(m_params.stas.cbegin(), + m_params.stas.cend(), + [maxChannelWidth](const auto& staInfo) { + return (staInfo.maxChannelWidth != maxChannelWidth); + }); + if (differentChannelWidths) + { + SpectrumWifiPhyHelper phyHelper; + phyHelper.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO); + + auto channel = CreateObject(); + phyHelper.SetChannel(channel); + + apDevice = wifi.Install(phyHelper, apMacHelper, wifiApNode); + auto staNodesIt = wifiStaNodes.Begin(); + for (const auto& staInfo : m_params.stas) + { + wifi.SetStandard(staInfo.standard); + staDevices.Add(wifi.Install(phyHelper, staMacHelper, *staNodesIt)); + ++staNodesIt; + } + + // Uncomment the lines below to write PCAP files + // phyHelper.EnablePcap("wifi-gcr_AP", apDevice); + // phyHelper.EnablePcap("wifi-gcr_STA", staDevices); + } + else + { + YansWifiPhyHelper phyHelper; + phyHelper.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO); + + auto channel = YansWifiChannelHelper::Default(); + channel.SetPropagationDelay("ns3::ConstantSpeedPropagationDelayModel"); + phyHelper.SetChannel(channel.Create()); + + apDevice = wifi.Install(phyHelper, apMacHelper, wifiApNode); + auto staNodesIt = wifiStaNodes.Begin(); + for (const auto& staInfo : m_params.stas) + { + wifi.SetStandard(staInfo.standard); + staDevices.Add(wifi.Install(phyHelper, staMacHelper, *staNodesIt)); + ++staNodesIt; + } + + // Uncomment the lines below to write PCAP files + // phyHelper.EnablePcap("wifi-gcr_AP", apDevice); + // phyHelper.EnablePcap("wifi-gcr_STA", staDevices); + } + + // Assign fixed streams to random variables in use + streamNumber += WifiHelper::AssignStreams(apDevice, streamNumber); + streamNumber += WifiHelper::AssignStreams(staDevices, streamNumber); + + MobilityHelper mobility; + Ptr positionAlloc = CreateObject(); + + positionAlloc->Add(Vector(0.0, 0.0, 0.0)); + for (uint8_t i = 0; i < numStas; ++i) + { + positionAlloc->Add(Vector(i, 0.0, 0.0)); + } + mobility.SetPositionAllocator(positionAlloc); + + mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel"); + mobility.Install(wifiApNode); + mobility.Install(wifiStaNodes); + + auto apNetDevice = DynamicCast(apDevice.Get(0)); + m_apWifiMac = DynamicCast(apNetDevice->GetMac()); + m_apWifiMac->SetAttribute("BE_MaxAmsduSize", UintegerValue(0)); + m_apWifiMac->SetAttribute( + "BE_MaxAmpduSize", + UintegerValue((m_params.maxNumMpdusInPsdu > 1) + ? (m_params.maxNumMpdusInPsdu * (m_params.packetSize + 100)) + : 0)); + + m_apWifiMac->SetAttribute("BE_BlockAckInactivityTimeout", + UintegerValue(m_params.baInactivityTimeout)); + m_apWifiMac->GetQosTxop(AC_BE)->SetTxopLimit(m_params.txopLimit); + + m_apWifiMac->GetWifiPhy(0)->SetOperatingChannel( + WifiPhy::ChannelTuple{0, maxChannelWidth, WIFI_PHY_BAND_5GHZ, 0}); + + m_apWifiMac->GetWifiPhy(0)->SetNumberOfAntennas(maxNss); + m_apWifiMac->GetWifiPhy(0)->SetMaxSupportedTxSpatialStreams(maxNss); + m_apWifiMac->GetWifiPhy(0)->SetMaxSupportedRxSpatialStreams(maxNss); + + m_apErrorModel = CreateObject(); + m_apWifiMac->GetWifiPhy(0)->SetPostReceptionErrorModel(m_apErrorModel); + + for (uint8_t i = 0; i < numStas; ++i) + { + auto staNetDevice = DynamicCast(staDevices.Get(i)); + auto staWifiMac = DynamicCast(staNetDevice->GetMac()); + staWifiMac->SetRobustAVStreamingSupported(m_params.stas.at(i).gcrCapable); + m_stasWifiMac.emplace_back(staWifiMac); + + if (m_params.stas.at(i).standard >= WIFI_STANDARD_80211n) + { + auto staHtConfiguration = CreateObject(); + staHtConfiguration->m_40MHzSupported = + (m_params.stas.at(i).standard >= WIFI_STANDARD_80211ac || + m_params.stas.at(i).maxChannelWidth >= 40); + staHtConfiguration->m_sgiSupported = (m_params.stas.at(i).minGi == NanoSeconds(400)); + staNetDevice->SetHtConfiguration(staHtConfiguration); + } + if (m_params.stas.at(i).standard >= WIFI_STANDARD_80211ac) + { + auto staVhtConfiguration = CreateObject(); + staVhtConfiguration->m_160MHzSupported = (m_params.stas.at(i).maxChannelWidth >= 160); + staNetDevice->SetVhtConfiguration(staVhtConfiguration); + } + if (m_params.stas.at(i).standard >= WIFI_STANDARD_80211ax) + { + auto staHeConfiguration = CreateObject(); + staHeConfiguration->SetGuardInterval( + std::max(m_params.stas.at(i).minGi, NanoSeconds(800))); + staNetDevice->SetHeConfiguration(staHeConfiguration); + } + + staWifiMac->GetWifiPhy(0)->SetOperatingChannel( + WifiPhy::ChannelTuple{0, m_params.stas.at(i).maxChannelWidth, WIFI_PHY_BAND_5GHZ, 0}); + + staWifiMac->GetWifiPhy(0)->SetNumberOfAntennas(m_params.stas.at(i).maxNumStreams); + staWifiMac->GetWifiPhy(0)->SetMaxSupportedTxSpatialStreams( + m_params.stas.at(i).maxNumStreams); + staWifiMac->GetWifiPhy(0)->SetMaxSupportedRxSpatialStreams( + m_params.stas.at(i).maxNumStreams); + + auto errorModel = CreateObject(); + m_errorModels.push_back(errorModel); + staWifiMac->GetWifiPhy(0)->SetPostReceptionErrorModel(errorModel); + + m_phyRxPerSta.push_back(0); + m_txCtsPerSta.push_back(0); + m_rxGroupcastPerSta.emplace_back(); + m_rxUnicastPerSta.emplace_back(); + } + + // give packet socket powers to nodes. + PacketSocketHelper packetSocket; + packetSocket.Install(wifiStaNodes); + packetSocket.Install(wifiApNode); + + if (m_params.numGroupcastPackets > 0) + { + PacketSocketAddress groupcastSocket; + groupcastSocket.SetSingleDevice(apDevice.Get(0)->GetIfIndex()); + groupcastSocket.SetPhysicalAddress( + Mac48Address::GetMulticast(Ipv4Address("239.192.100.1"))); + groupcastSocket.SetProtocol(MULTICAST_PROTOCOL); + + m_groupcastClient = CreateObject(); + m_groupcastClient->SetAttribute("MaxPackets", UintegerValue(m_params.numGroupcastPackets)); + m_groupcastClient->SetAttribute("PacketSize", UintegerValue(m_params.packetSize)); + m_groupcastClient->SetAttribute("Interval", TimeValue(Seconds(0))); + m_groupcastClient->SetRemote(groupcastSocket); + wifiApNode.Get(0)->AddApplication(m_groupcastClient); + m_groupcastClient->SetStartTime(m_params.startGroupcast); + m_groupcastClient->SetStopTime(m_params.duration); + + for (uint8_t i = 0; i < numStas; ++i) + { + auto groupcastServer = CreateObject(); + groupcastServer->SetLocal(groupcastSocket); + wifiStaNodes.Get(i)->AddApplication(groupcastServer); + groupcastServer->SetStartTime(Seconds(0.0)); + groupcastServer->SetStopTime(m_params.duration); + } + } + + if (m_params.numUnicastPackets > 0) + { + uint8_t staIndex = 0; + for (uint8_t i = 0; i < numStas; ++i) + { + PacketSocketAddress unicastSocket; + unicastSocket.SetSingleDevice(apDevice.Get(0)->GetIfIndex()); + unicastSocket.SetPhysicalAddress(staDevices.Get(staIndex++)->GetAddress()); + unicastSocket.SetProtocol(UNICAST_PROTOCOL); + + auto unicastClient = CreateObject(); + unicastClient->SetAttribute("PacketSize", UintegerValue(m_params.packetSize)); + unicastClient->SetAttribute("MaxPackets", UintegerValue(m_params.numUnicastPackets)); + unicastClient->SetAttribute("Interval", TimeValue(Seconds(0))); + unicastClient->SetRemote(unicastSocket); + wifiApNode.Get(0)->AddApplication(unicastClient); + unicastClient->SetStartTime(m_params.startUnicast); + unicastClient->SetStopTime(m_params.duration); + + auto unicastServer = CreateObject(); + unicastServer->SetLocal(unicastSocket); + wifiStaNodes.Get(i)->AddApplication(unicastServer); + unicastServer->SetStartTime(Seconds(0.0)); + unicastServer->SetStopTime(m_params.duration); + } + } + + PointerValue ptr; + m_apWifiMac->GetAttribute("BE_Txop", ptr); + ptr.Get()->TraceConnectWithoutContext( + "TxopTrace", + MakeCallback(&GcrTestBase::NotifyTxopTerminated, this)); + + Config::Connect("/NodeList/*/$ns3::Node/ApplicationList/*/$ns3::PacketSocketClient/Tx", + MakeCallback(&GcrTestBase::PacketGenerated, this)); + + Config::Connect("/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/Phys/0/PhyTxPsduBegin", + MakeCallback(&GcrTestBase::Transmit, this)); + + Config::Connect("/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/Phy/$ns3::WifiPhy/State/RxOk", + MakeCallback(&GcrTestBase::PhyRx, this)); + + Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSocketServer/Rx", + MakeCallback(&GcrTestBase::Receive, this)); +} + +void +GcrTestBase::DoRun() +{ + NS_LOG_FUNCTION(this << m_testName); + + Simulator::Stop(m_params.duration); + Simulator::Run(); + + CheckResults(); + + Simulator::Destroy(); +} + +GcrUrTest::GcrUrTest(const std::string& testName, + const GcrParameters& commonParams, + const GcrUrParameters& gcrUrParams) + : GcrTestBase(testName, commonParams), + m_gcrUrParams{gcrUrParams}, + m_currentUid{0} +{ +} + +void +GcrUrTest::PacketGenerated(std::string context, Ptr p, const Address& addr) +{ + if (!m_gcrUrParams.packetsPauzeAggregation.has_value() || + m_packets < m_gcrUrParams.packetsPauzeAggregation.value() || + m_packets > m_gcrUrParams.packetsResumeAggregation.value()) + { + GcrTestBase::PacketGenerated(context, p, addr); + return; + } + m_packets++; + if (m_packets == (m_gcrUrParams.packetsPauzeAggregation.value() + 1)) + { + m_groupcastClient->SetAttribute("Interval", TimeValue(MilliSeconds(10))); + } + if (m_gcrUrParams.packetsResumeAggregation.has_value() && + (m_packets == (m_gcrUrParams.packetsResumeAggregation.value() + 1))) + { + m_groupcastClient->SetAttribute("Interval", TimeValue(MilliSeconds(0))); + } +} + +void +GcrUrTest::Transmit(std::string context, + WifiConstPsduMap psduMap, + WifiTxVector txVector, + double txPowerW) +{ + auto psdu = psduMap.cbegin()->second; + auto mpdu = *psdu->begin(); + auto addr1 = mpdu->GetHeader().GetAddr1(); + if (addr1.IsGroup() && !addr1.IsBroadcast() && mpdu->GetHeader().IsQosData()) + { + if (const auto uid = mpdu->GetPacket()->GetUid(); m_currentUid != uid) + { + m_totalTxGroupcasts.push_back(0); + m_currentUid = uid; + m_currentMpdu = nullptr; + } + if (m_totalTxGroupcasts.back() == 0) + { + NS_LOG_INFO("AP: groupcast initial transmission (#MPDUs=" << psdu->GetNMpdus() << ")"); + for (std::size_t i = 0; i < psdu->GetNMpdus(); ++i) + { + NS_TEST_EXPECT_MSG_EQ( + psdu->GetHeader(i).IsRetry(), + false, + "retry flag should not be set for the first groupcast transmission"); + } + m_currentMpdu = mpdu; + } + else + { + NS_ASSERT(m_currentMpdu); + NS_TEST_EXPECT_MSG_EQ(m_expectGcrUsed, true, "GCR service should not be used"); + NS_LOG_INFO("AP: groupcast sollicited retry #" + << +m_totalTxGroupcasts.back() << " (#MPDUs=" << psdu->GetNMpdus() << ")"); + for (std::size_t i = 0; i < psdu->GetNMpdus(); ++i) + { + NS_TEST_EXPECT_MSG_EQ(psdu->GetHeader(i).IsRetry(), + true, + "retry flag should be set for unsolicited retries"); + } + NS_TEST_EXPECT_MSG_EQ((mpdu->GetHeader().IsQosAmsdu() ? mpdu->begin()->first->GetSize() + : mpdu->GetPacket()->GetSize()), + (m_currentMpdu->GetHeader().IsQosAmsdu() + ? m_currentMpdu->begin()->first->GetSize() + : m_currentMpdu->GetPacket()->GetSize()), + "Unexpected MPDU size"); + } + if (m_params.maxNumMpdusInPsdu > 1) + { + const uint16_t prevTxMpdus = + (m_totalTxGroupcasts.size() - 1) * m_expectedMaxNumMpdusInPsdu; + const uint16_t remainingMpdus = m_gcrUrParams.packetsPauzeAggregation.has_value() + ? m_params.numGroupcastPackets + : m_params.numGroupcastPackets - prevTxMpdus; + NS_TEST_EXPECT_MSG_EQ( + psdu->GetNMpdus(), + (IsUsingAmpduOrSmpdu() ? std::min(m_expectedMaxNumMpdusInPsdu, remainingMpdus) : 1), + "Incorrect number of aggregated MPDUs"); + const auto nonAggregatedMpdus = (m_gcrUrParams.packetsResumeAggregation.value_or(0) - + m_gcrUrParams.packetsPauzeAggregation.value_or(0)); + const uint16_t threshold = + (m_gcrUrParams.packetsPauzeAggregation.value_or(0) / m_params.maxNumMpdusInPsdu) + + nonAggregatedMpdus; + for (std::size_t i = 0; i < psdu->GetNMpdus(); ++i) + { + auto previousMpdusNotAggregated = + (m_totalTxGroupcasts.size() > threshold) ? nonAggregatedMpdus : 0; + auto expectedSeqNum = IsUsingAmpduOrSmpdu() + ? ((i + prevTxMpdus) - previousMpdusNotAggregated) + : (((m_totalTxGroupcasts.size() - 1) + + (m_gcrUrParams.packetsPauzeAggregation.value_or(0) / + m_params.maxNumMpdusInPsdu)) - + previousMpdusNotAggregated); + NS_TEST_EXPECT_MSG_EQ(psdu->GetHeader(i).GetSequenceNumber(), + expectedSeqNum, + "unexpected sequence number"); + } + } + else + { + NS_TEST_EXPECT_MSG_EQ(psdu->GetNMpdus(), 1, "MPDU aggregation should not be used"); + NS_TEST_EXPECT_MSG_EQ(mpdu->GetHeader().GetSequenceNumber(), + (m_totalTxGroupcasts.size() - 1), + "unexpected sequence number"); + } + m_totalTxGroupcasts.back()++; + m_nTxGroupcastInCurrentTxop++; + } + else if (mpdu->GetHeader().IsRts()) + { + m_nTxRtsInCurrentTxop++; + } + else if (const auto nodeId = ConvertContextToNodeId(context); + (mpdu->GetHeader().IsCts() && (nodeId == 0))) + { + m_nTxCtsInCurrentTxop++; + } + GcrTestBase::Transmit(context, psduMap, txVector, txPowerW); +} + +void +GcrUrTest::Receive(std::string context, Ptr p, const Address& adr) +{ + const auto staId = ConvertContextToNodeId(context) - 1; + NS_LOG_INFO("STA" << staId + 1 << ": multicast packet forwarded up at attempt " + << +m_totalTxGroupcasts.back()); + m_rxGroupcastPerSta.at(staId).push_back(m_totalTxGroupcasts.back()); +} + +void +GcrUrTest::ConfigureGcrManager(WifiMacHelper& macHelper) +{ + macHelper.SetGcrManager("ns3::WifiDefaultGcrManager", + "RetransmissionPolicy", + StringValue("GCR_UR"), + "UnsolicitedRetryLimit", + UintegerValue(m_gcrUrParams.nGcrRetries), + "GcrProtectionMode", + EnumValue(m_params.gcrProtectionMode)); +} + +bool +GcrUrTest::IsUsingAmpduOrSmpdu() const +{ + if (!GcrTestBase::IsUsingAmpduOrSmpdu()) + { + return false; + } + if (GetNumNonHtStas(m_params.stas) > 0) + { + return false; + } + const auto nonAggregatedMpdus = (m_gcrUrParams.packetsResumeAggregation.value_or(0) - + m_gcrUrParams.packetsPauzeAggregation.value_or(0)); + const uint16_t threshold = + (m_gcrUrParams.packetsPauzeAggregation.value_or(0) / m_params.maxNumMpdusInPsdu) + + nonAggregatedMpdus; + const auto expectAmpdu = + (!m_gcrUrParams.packetsPauzeAggregation.has_value() || + (m_totalTxGroupcasts.size() <= + (m_gcrUrParams.packetsPauzeAggregation.value() / m_params.maxNumMpdusInPsdu)) || + (m_totalTxGroupcasts.size() > threshold)); + return expectAmpdu; +} + +void +GcrUrTest::CheckResults() +{ + GcrTestBase::CheckResults(); + + const auto expectedMaxNumMpdusInPsdu = + (GetNumNonHtStas(m_params.stas) == 0) ? m_expectedMaxNumMpdusInPsdu : 1; + const std::size_t numNonRetryGroupcastFrames = + m_gcrUrParams.packetsPauzeAggregation.has_value() + ? (m_params.numGroupcastPackets - + std::ceil(static_cast(m_gcrUrParams.packetsPauzeAggregation.value()) / + expectedMaxNumMpdusInPsdu) - + std::ceil(static_cast(m_params.numGroupcastPackets - + m_gcrUrParams.packetsResumeAggregation.value()) / + expectedMaxNumMpdusInPsdu)) + : std::ceil(static_cast(m_params.numGroupcastPackets - + m_params.expectedDroppedGroupcastMpdus.size()) / + expectedMaxNumMpdusInPsdu); + NS_TEST_EXPECT_MSG_EQ(m_totalTxGroupcasts.size(), + numNonRetryGroupcastFrames, + "Unexpected number of non-retransmitted groupcast frames"); + + NS_ASSERT(!m_totalTxGroupcasts.empty()); + const auto totalTxGroupcastFrames = + std::accumulate(m_totalTxGroupcasts.cbegin(), m_totalTxGroupcasts.cend(), 0U); + uint8_t numRetries = m_expectGcrUsed ? m_gcrUrParams.nGcrRetries : 0; + // with test conditions, one more retry when A-MPDU is not used + const auto nonAmpduPackets = m_gcrUrParams.packetsPauzeAggregation.has_value() + ? (m_gcrUrParams.packetsResumeAggregation.value() - + m_gcrUrParams.packetsPauzeAggregation.value()) + : 0; + ; + uint16_t expectedTxAttempts = + m_gcrUrParams.packetsPauzeAggregation.has_value() && + (m_gcrUrParams.expectedSkippedRetries > 0) + ? (std::ceil((1 + numRetries - m_gcrUrParams.expectedSkippedRetries) * + ((static_cast(m_params.numGroupcastPackets - nonAmpduPackets) / + expectedMaxNumMpdusInPsdu))) + + ((1 + numRetries - (m_gcrUrParams.expectedSkippedRetries - 1)) * nonAmpduPackets)) + : ((1 + numRetries - m_gcrUrParams.expectedSkippedRetries) * + numNonRetryGroupcastFrames); + NS_TEST_EXPECT_MSG_EQ(totalTxGroupcastFrames, + expectedTxAttempts, + "Unexpected number of transmission attempts"); + + uint8_t numStas = m_params.stas.size(); + for (uint8_t i = 0; i < numStas; ++i) + { + numRetries = + (m_params.stas.at(i).standard >= WIFI_STANDARD_80211n) ? m_gcrUrParams.nGcrRetries : 0; + expectedTxAttempts = + m_gcrUrParams.packetsPauzeAggregation.has_value() && + (m_gcrUrParams.expectedSkippedRetries > 0) + ? (std::ceil((1 + numRetries - m_gcrUrParams.expectedSkippedRetries) * + ((static_cast(m_params.numGroupcastPackets - nonAmpduPackets) / + expectedMaxNumMpdusInPsdu))) + + ((1 + numRetries - (m_gcrUrParams.expectedSkippedRetries - 1)) * + nonAmpduPackets)) + : ((1 + numRetries - m_gcrUrParams.expectedSkippedRetries) * + numNonRetryGroupcastFrames); + + // calculate the amount of corrupted PSDUs and the expected number of retransmission per + // MPDU + uint8_t corruptedPsdus = 0; + uint8_t prevExpectedNumAttempt = 1; + uint8_t prevPsduNum = 1; + uint8_t droppedPsdus = 0; + auto prevDropped = false; + auto maxNumMpdusInPsdu = + (GetNumNonHtStas(m_params.stas) == 0) ? m_params.maxNumMpdusInPsdu : 1; + for (uint16_t j = 0; j < m_params.numGroupcastPackets; ++j) + { + uint8_t expectedNumAttempt = 1; + const auto psduNum = ((j / maxNumMpdusInPsdu) + 1); + const auto packetInAmpdu = (maxNumMpdusInPsdu > 1) ? ((j % maxNumMpdusInPsdu) + 1) : 1; + if (psduNum > prevPsduNum) + { + prevExpectedNumAttempt = 1; + prevDropped = false; + } + prevPsduNum = psduNum; + for (auto& mpduToCorruptPerPsdu : m_params.mpdusToCorruptPerPsdu) + { + if (mpduToCorruptPerPsdu.first <= ((psduNum - 1) * (1 + numRetries))) + { + continue; + } + if (mpduToCorruptPerPsdu.first > (psduNum * (1 + numRetries))) + { + continue; + } + if (((GetNumGcrStas(m_params.stas) > 0) && GetNumNonHtStas(m_params.stas) > 0) && + (numRetries == 0) && + (mpduToCorruptPerPsdu.first % m_gcrUrParams.nGcrRetries) != 1) + { + continue; + } + const auto& corruptedMpdusForSta = + (mpduToCorruptPerPsdu.second.count(0) != 0) + ? mpduToCorruptPerPsdu.second.at(0) + : ((mpduToCorruptPerPsdu.second.count(i + 1) != 0) + ? mpduToCorruptPerPsdu.second.at(i + 1) + : std::set{}); + if (corruptedMpdusForSta.count(packetInAmpdu) == 0) + { + break; + } + if ((maxNumMpdusInPsdu == 1) || + ((corruptedMpdusForSta.size() == 2) && (packetInAmpdu == 2))) + { + corruptedPsdus++; + } + expectedNumAttempt++; + } + if (const auto nMaxAttempts = + m_params.stas.at(i).gcrCapable ? (m_gcrUrParams.nGcrRetries + 1) : 1; + (expectedNumAttempt > nMaxAttempts) || + (m_params.expectedDroppedGroupcastMpdus.count(j + 1) != 0)) + { + droppedPsdus++; + prevDropped = true; + continue; + } + expectedNumAttempt = (prevDropped && (psduNum < 2)) + ? 1 + : std::max(expectedNumAttempt, prevExpectedNumAttempt); + prevExpectedNumAttempt = expectedNumAttempt; + const std::size_t rxPsdus = (j - droppedPsdus); + NS_ASSERT(m_rxGroupcastPerSta.at(i).size() > rxPsdus); + NS_TEST_EXPECT_MSG_EQ(+m_rxGroupcastPerSta.at(i).at(rxPsdus), + +expectedNumAttempt, + "Packet has not been forwarded up at the expected TX attempt"); + } + const std::size_t rxPackets = (m_params.numGroupcastPackets - droppedPsdus); + NS_TEST_EXPECT_MSG_EQ(+m_rxGroupcastPerSta.at(i).size(), + rxPackets, + "STA" + std::to_string(i + 1) + + " did not receive the expected number of groupcast packets"); + NS_TEST_EXPECT_MSG_EQ(+m_phyRxPerSta.at(i), + (expectedTxAttempts - corruptedPsdus), + "STA" + std::to_string(i + 1) + + " did not receive the expected number of retransmissions"); + } +} + +WifiGcrTestSuite::WifiGcrTestSuite() + : TestSuite("wifi-gcr", Type::UNIT) +{ + using StationsScenarios = std::vector>; + + // GCR Unsolicited Retries + for (auto& [useAmpdu, ampduScenario] : + std::vector>{{false, "A-MPDU disabled"}, + {true, "A-MPDU enabled"}}) + { + for (auto& [rtsThreshold, gcrPotection, protectionName] : + std::vector>{ + {maxRtsCtsThreshold, GroupcastProtectionMode::RTS_CTS, "no protection"}, + {500, GroupcastProtectionMode::RTS_CTS, "RTS-CTS"}, + {1500, GroupcastProtectionMode::CTS_TO_SELF, "CTS-TO-SELF"}}) + { + for (const auto& stasInfo : StationsScenarios{ + {{{GCR_INCAPABLE_STA, WIFI_STANDARD_80211a}}}, + {{{GCR_CAPABLE_STA, WIFI_STANDARD_80211n, 40, 2, NanoSeconds(400)}}}, + {{{GCR_CAPABLE_STA, WIFI_STANDARD_80211ac}}}, + {{{GCR_CAPABLE_STA, WIFI_STANDARD_80211ax}}}, + {{{GCR_CAPABLE_STA, WIFI_STANDARD_80211be}}}, + {{{GCR_CAPABLE_STA, WIFI_STANDARD_80211ax, 80, 1, NanoSeconds(800)}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211be, 80, 1, NanoSeconds(3200)}}}, + {{{GCR_CAPABLE_STA, WIFI_STANDARD_80211n, 20, 1}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211ac, 80, 2}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211ax, 160, 3}}}, + {{{GCR_INCAPABLE_STA, WIFI_STANDARD_80211a}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211n}}}, + {{{GCR_INCAPABLE_STA, WIFI_STANDARD_80211n}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211n}}}}) + { + const auto maxChannelWidth = + std::max_element(stasInfo.cbegin(), + stasInfo.cend(), + [](const auto& lhs, const auto& rhs) { + return lhs.maxChannelWidth < rhs.maxChannelWidth; + }) + ->maxChannelWidth; + const auto useSpectrum = + std::any_of(stasInfo.cbegin(), + stasInfo.cend(), + [maxChannelWidth](const auto& staInfo) { + return (staInfo.maxChannelWidth != maxChannelWidth); + }); + const std::string scenario = "STAs=" + printStasInfo(stasInfo) + + ", protection=" + protectionName + ", " + + ampduScenario; + AddTestCase( + new GcrUrTest("GCR-UR without any lost frames: " + scenario, + {.stas = stasInfo, + .numGroupcastPackets = useAmpdu ? uint16_t(4) : uint16_t(2), + .maxNumMpdusInPsdu = useAmpdu ? uint16_t(2) : uint16_t(1), + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with first frame lost: " + scenario, + {.stas = stasInfo, + .numGroupcastPackets = useAmpdu ? uint16_t(4) : uint16_t(2), + .maxNumMpdusInPsdu = useAmpdu ? uint16_t(2) : uint16_t(1), + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + // if no MPDU aggregation, MPDUs list is ignored + .mpdusToCorruptPerPsdu = {{1, {{0, {1, 2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all but last frame lost: " + scenario, + {.stas = stasInfo, + .numGroupcastPackets = useAmpdu ? uint16_t(4) : uint16_t(2), + .maxNumMpdusInPsdu = useAmpdu ? uint16_t(2) : uint16_t(1), + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + // if no MPDU aggregation, MPDUs list is ignored + .mpdusToCorruptPerPsdu = {{1, {{0, {1, 2}}}}, + {2, {{0, {1, 2}}}}, + {3, {{0, {1, 2}}}}, + {4, {{0, {1, 2}}}}, + {5, {{0, {1, 2}}}}, + {6, {{0, {1, 2}}}}, + {7, {{0, {1, 2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all frames lost: " + scenario, + {.stas = stasInfo, + .numGroupcastPackets = useAmpdu ? uint16_t(4) : uint16_t(2), + .maxNumMpdusInPsdu = useAmpdu ? uint16_t(2) : uint16_t(1), + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + // if no MPDU aggregation, MPDUs list is ignored + .mpdusToCorruptPerPsdu = {{1, {{0, {1, 2}}}}, + {2, {{0, {1, 2}}}}, + {3, {{0, {1, 2}}}}, + {4, {{0, {1, 2}}}}, + {5, {{0, {1, 2}}}}, + {6, {{0, {1, 2}}}}, + {7, {{0, {1, 2}}}}, + {8, {{0, {1, 2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + if ((GetNumNonGcrStas(stasInfo) == 0) && useAmpdu) + { + AddTestCase( + new GcrUrTest("GCR-UR with 1 MPDU always corrupted in first A-MPDU but one " + "different MPDU alternatively, starting with second MPDU: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{1, {{0, {2}}}}, + {2, {{0, {1}}}}, + {3, {{0, {2}}}}, + {4, {{0, {1}}}}, + {5, {{0, {2}}}}, + {6, {{0, {1}}}}, + {7, {{0, {2}}}}, + {8, {{0, {1}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with 1 MPDU always corrupted in first A-MPDU but one " + "different MPDU alternatively, starting with first MPDU: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{1, {{0, {1}}}}, + {2, {{0, {2}}}}, + {3, {{0, {1}}}}, + {4, {{0, {2}}}}, + {5, {{0, {1}}}}, + {6, {{0, {2}}}}, + {7, {{0, {1}}}}, + {8, {{0, {2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all MPDUs always corrupted in first A-MPDU " + "except the first MPDU in the last retransmission: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{1, {{0, {1, 2}}}}, + {2, {{0, {1, 2}}}}, + {3, {{0, {1, 2}}}}, + {4, {{0, {1, 2}}}}, + {5, {{0, {1, 2}}}}, + {6, {{0, {1, 2}}}}, + {7, {{0, {1, 2}}}}, + {8, {{0, {2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all MPDUs always corrupted in first A-MPDU " + "except the second MPDU in the last retransmission: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{1, {{0, {1, 2}}}}, + {2, {{0, {1, 2}}}}, + {3, {{0, {1, 2}}}}, + {4, {{0, {1, 2}}}}, + {5, {{0, {1, 2}}}}, + {6, {{0, {1, 2}}}}, + {7, {{0, {1, 2}}}}, + {8, {{0, {1}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all MPDUs always corrupted in first A-MPDU: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{1, {{0, {1, 2}}}}, + {2, {{0, {1, 2}}}}, + {3, {{0, {1, 2}}}}, + {4, {{0, {1, 2}}}}, + {5, {{0, {1, 2}}}}, + {6, {{0, {1, 2}}}}, + {7, {{0, {1, 2}}}}, + {8, {{0, {1, 2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest( + "GCR-UR with 1 MPDU always corrupted in second A-MPDU but one " + "different MPDU alternatively, starting with second MPDU: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{9, {{0, {2}}}}, + {10, {{0, {1}}}}, + {11, {{0, {2}}}}, + {12, {{0, {1}}}}, + {13, {{0, {2}}}}, + {14, {{0, {1}}}}, + {15, {{0, {2}}}}, + {16, {{0, {1}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE + : TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest( + "GCR-UR with 1 MPDU always corrupted in second A-MPDU but one " + "different MPDU alternatively, starting with first MPDU: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{9, {{0, {1}}}}, + {10, {{0, {2}}}}, + {11, {{0, {1}}}}, + {12, {{0, {2}}}}, + {13, {{0, {1}}}}, + {14, {{0, {2}}}}, + {15, {{0, {1}}}}, + {16, {{0, {2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE + : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all MPDUs always corrupted in second A-MPDU " + "except the first MPDU in the last retransmission: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{9, {{0, {1, 2}}}}, + {10, {{0, {1, 2}}}}, + {11, {{0, {1, 2}}}}, + {12, {{0, {1, 2}}}}, + {13, {{0, {1, 2}}}}, + {14, {{0, {1, 2}}}}, + {15, {{0, {1, 2}}}}, + {16, {{0, {2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all MPDUs always corrupted in second A-MPDU " + "except the second MPDU in the last retransmission: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{9, {{0, {1, 2}}}}, + {10, {{0, {1, 2}}}}, + {11, {{0, {1, 2}}}}, + {12, {{0, {1, 2}}}}, + {13, {{0, {1, 2}}}}, + {14, {{0, {1, 2}}}}, + {15, {{0, {1, 2}}}}, + {16, {{0, {1}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + AddTestCase( + new GcrUrTest("GCR-UR with all MPDUs always corrupted in second A-MPDU: " + + scenario, + {.stas = stasInfo, + .numGroupcastPackets = 4, + .maxNumMpdusInPsdu = 2, + .rtsThreshold = rtsThreshold, + .gcrProtectionMode = gcrPotection, + .mpdusToCorruptPerPsdu = {{9, {{0, {1, 2}}}}, + {10, {{0, {1, 2}}}}, + {11, {{0, {1, 2}}}}, + {12, {{0, {1, 2}}}}, + {13, {{0, {1, 2}}}}, + {14, {{0, {1, 2}}}}, + {15, {{0, {1, 2}}}}, + {16, {{0, {1, 2}}}}}}, + {}), + useSpectrum ? TestCase::Duration::EXTENSIVE : TestCase::Duration::QUICK); + } + } + } + } + AddTestCase(new GcrUrTest("GCR-UR with 4 skipped retries because of lifetime limit", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211n}}, + .numGroupcastPackets = 1, + .maxNumMpdusInPsdu = 1, + .maxLifetime = MilliSeconds(1), + .rtsThreshold = maxRtsCtsThreshold}, + {.expectedSkippedRetries = 4}), + TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest("GCR-UR with A-MPDU paused during test and number of packets larger " + "than MPDU buffer size", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211n}}, + .numGroupcastPackets = 300, + .maxNumMpdusInPsdu = 2, + .startGroupcast = Seconds(1.0), + .maxLifetime = MilliSeconds(500), + .rtsThreshold = maxRtsCtsThreshold, + .duration = Seconds(3.0)}, + {.packetsPauzeAggregation = 4, .packetsResumeAggregation = 100}), + TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest("GCR-UR with buffer size limit to 64 MPDUs", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211n}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211ac}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211ax}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211be}}, + .numGroupcastPackets = 300, + .packetSize = 200, + .maxNumMpdusInPsdu = 1024, // capped to 64 because not lowest is HT + .rtsThreshold = maxRtsCtsThreshold}, + {}), + TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest("GCR-UR with buffer size limit to 256 MPDUs", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211ax}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211ax}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211be}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211be}}, + .numGroupcastPackets = 300, + .packetSize = 200, + .maxNumMpdusInPsdu = 1024, // capped to 256 because not lowest is HE + .rtsThreshold = maxRtsCtsThreshold}, + {}), + TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest("GCR-UR with buffer size limit to 1024 MPDUs", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211be, 40}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211be, 40}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211be, 40}, + {GCR_CAPABLE_STA, WIFI_STANDARD_80211be, 40}}, + .numGroupcastPackets = 1200, + .packetSize = 100, + .maxNumMpdusInPsdu = 1024, + .rtsThreshold = maxRtsCtsThreshold}, + {}), + TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest("GCR-UR with corrupted RTS frames to verify previously assigned " + "sequence numbers are properly released", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211n}}, + .numGroupcastPackets = 6, + .packetSize = 500, + .maxNumMpdusInPsdu = 2, + .maxLifetime = MilliSeconds( + 1), // reduce lifetime to make sure packets get dropped + .rtsThreshold = 500, + .rtsFramesToCorrupt = {3, 4, 5}, + .expectedDroppedGroupcastMpdus = {3, 4}}, + {.expectedSkippedRetries = 6}), + TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest("GCR-UR with corrupted CTS frames to verify previously assigned " + "sequence numbers are properly released", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211n}}, + .numGroupcastPackets = 6, + .packetSize = 500, + .maxNumMpdusInPsdu = 2, + .maxLifetime = MilliSeconds( + 1), // reduce lifetime to make sure packets get dropped + .rtsThreshold = 500, + .ctsFramesToCorrupt = {3, 4, 5}, + .expectedDroppedGroupcastMpdus = {3, 4}}, + {.expectedSkippedRetries = 6}), + TestCase::Duration::QUICK); + AddTestCase(new GcrUrTest("GCR-UR with reduced lifetime, A-MPDU paused during test and number " + "of packets larger than MPDU buffer size", + {.stas = {{GCR_CAPABLE_STA, WIFI_STANDARD_80211n}}, + .numGroupcastPackets = 300, + .packetSize = 500, + .maxNumMpdusInPsdu = 2, + .maxLifetime = MilliSeconds(1), + .rtsThreshold = maxRtsCtsThreshold, + .duration = Seconds(4.0)}, + {.expectedSkippedRetries = 4, + .packetsPauzeAggregation = 4, + .packetsResumeAggregation = 100}), + TestCase::Duration::QUICK); +} + +static WifiGcrTestSuite g_wifiGcrTestSuite; ///< the test suite diff --git a/src/wifi/test/wifi-gcr-test.h b/src/wifi/test/wifi-gcr-test.h new file mode 100644 index 0000000000..9ec4137097 --- /dev/null +++ b/src/wifi/test/wifi-gcr-test.h @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2023 DERONNE SOFTWARE ENGINEERING + * + * SPDX-License-Identifier: GPL-2.0-only + * + * Author: Sébastien Deronne + */ + +#ifndef WIFI_GCR_TEST_H +#define WIFI_GCR_TEST_H + +#include "ns3/ap-wifi-mac.h" +#include "ns3/error-model.h" +#include "ns3/ht-phy.h" +#include "ns3/packet-socket-client.h" +#include "ns3/sta-wifi-mac.h" +#include "ns3/test.h" +#include "ns3/wifi-default-gcr-manager.h" +#include "ns3/wifi-mac-helper.h" + +#include +#include +#include +#include +#include + +using namespace ns3; + +/** + * @ingroup wifi-test + * @ingroup tests + * + * @brief Base class for GCR tests + * + * It considers an AP and multiple STAs (with different capabilities) using either GCR-UR or GCR-BA. + * The AP generates either multicast packets only or alternatively multicast and unicast packets. + * + * The test eventually corrupts some MPDUs based on a provided list of groupcast MPDUs in a given + * PSDU (indices are starting from 1) that should not be successfully received by a given STA or by + * all STA (s). It may also corrupts specific frames, such as RTS/CTS or action frames that are used + * to establish or teardown Block Ack agreements. The latter is needed is needed when A-MPDU is + * used. + * + * It is checked that: + * + * - When no GCR-capable STA is present, GCR service is not used + * - When the GCR service is used, groupcast frames are transmitted using HT, VHT or HE modulation + * class, depending on the supported modulations by member STAs + * - When the GCR is used, groupcast MPDUs carry an A-MSDU made of a single A-MSDU subframe, + * regardless of the A-MSDU size settings + * - When the GCR service is used, the expected protection mechanism is being used prior to the + * transmission of the groupcast packet + * - When the GCR service is used and RTS/CTS protection is selected, the receiver address of RTS + * frames corresponds to the MAC address of one of the STA of the group + * - When GCR-BA or GCR-UR with agreement is used, the expected amount of ADDBA request and response + * frames has been received and they all contain the GCR group address + * - when Block Ack agreement timeout is used, the expected amount of DELBA frames has been received + * and they all contain the GCR group address + */ +class GcrTestBase : public TestCase +{ + public: + /// Information about GCR STAs + struct StaInfo + { + bool gcrCapable{false}; ///< flag whether the STA is GCR capable + WifiStandard standard{WIFI_STANDARD_UNSPECIFIED}; ///< standard configured for the STA + MHz_u maxChannelWidth{20}; ///< maximum channel width in MHz supported by the STA + uint8_t maxNumStreams{1}; ///< maximum number of spatial streams supported by the STA + Time minGi{NanoSeconds(800)}; ///< minimum guard interval duration supported by the STA + }; + + /// Common parameters for GCR tests + struct GcrParameters + { + std::vector stas{}; ///< information about STAs + uint16_t numGroupcastPackets{0}; ///< number of groupcast packets to generate + uint16_t numUnicastPackets{0}; ///< number of unicast packets to generate + uint32_t packetSize{1000}; ///< the size in bytes of the packets to generate + uint16_t maxNumMpdusInPsdu{0}; ///< maximum number of MPDUs in PSDUs + Time startGroupcast{Seconds(1.0)}; ///< time to start groupcast packets generation + Time startUnicast{}; ///< time to start unicast packets generation + Time maxLifetime{MilliSeconds(500)}; ///< the maximum MSDU lifetime + uint32_t rtsThreshold{}; ///< the RTS threshold in bytes + GroupcastProtectionMode gcrProtectionMode{ + GroupcastProtectionMode::RTS_CTS}; ///< the protection mode to use + std::map>> + mpdusToCorruptPerPsdu{}; ///< list of MPDUs (starting from 1) to corrupt per PSDU + ///< (starting from 1) + std::set rtsFramesToCorrupt{}; ///< list of RTS frames (starting from 1) to corrupt + std::set ctsFramesToCorrupt{}; ///< list of CTS frames (starting from 1) to corrupt + std::set + addbaReqsToCorrupt{}; ///< list of GCR ADDBA requests (starting from 1) to corrupt + std::set + addbaRespsToCorrupt{}; ///< list of GCR ADDBA responses (starting from 1) to corrupt + std::set expectedDroppedGroupcastMpdus{}; ///< list of groupcast MPDUs that are + ///< expected to be dropped because of + ///< lifetime expiry (starting from 1) + uint16_t baInactivityTimeout{0}; ///< max time (blocks of 1024 microseconds) allowed for + ///< block ack inactivity + Time txopLimit{}; ///< the TXOP limit duration + Time duration{ + Seconds(2)}; ///< the duration of the simulation for the test run (2 seconds by default) + }; + + /** + * Constructor + * + * @param testName the name of the test + * @param params the common GCR parameters for the test to run + */ + GcrTestBase(const std::string& testName, const GcrParameters& params); + ~GcrTestBase() override = default; + + protected: + void DoSetup() override; + void DoRun() override; + + /** + * Check results at the end of the test run. + */ + virtual void CheckResults(); + + /** + * Configure the GCR manager for the test. + * + * @param macHelper the wifi mac helper + */ + virtual void ConfigureGcrManager(WifiMacHelper& macHelper) = 0; + + /** + * Callback invoked when a packet is generated by the packet socket client. + * + * @param context the context + * @param p the packet + * @param adr the address + */ + virtual void PacketGenerated(std::string context, Ptr p, const Address& adr); + + /** + * Callback invoked when a FEM passes PSDUs to the PHY. + * + * @param context the context + * @param psduMap the PSDU map + * @param txVector the TX vector + * @param txPowerW the tx power in Watts + */ + virtual void Transmit(std::string context, + WifiConstPsduMap psduMap, + WifiTxVector txVector, + double txPowerW); + + /** + * Callback invoked when a packet is successfully received by the PHY. + * + * @param context the context + * @param p the packet + * @param snr the SNR (in linear scale) + * @param mode the WiFi mode + * @param preamble the preamble + */ + virtual void PhyRx(std::string context, + Ptr p, + double snr, + WifiMode mode, + WifiPreamble preamble); + + /** + * Callback invoked when packet is received by the packet socket server. + * + * @param context the context + * @param p the packet + * @param adr the address + */ + virtual void Receive(std::string context, Ptr p, const Address& adr) = 0; + + /** + * Callback invoked when a TXOP is terminated. + * + * @param startTime the time TXOP started + * @param duration the duration of the TXOP + * @param linkId the ID of the link that gained TXOP + */ + virtual void NotifyTxopTerminated(Time startTime, Time duration, uint8_t linkId); + + /** + * Function to indicate whether A-MPDU or S-MPDU is currently being used. + * + * @return true if A-MPDU or S-MPDU is currently being used, false otherwise + */ + virtual bool IsUsingAmpduOrSmpdu() const; + + std::string m_testName; ///< name of the test + GcrParameters m_params; ///< parameters for the test to run + bool m_expectGcrUsed; ///< flag whether GCR is expected to be used during the test + uint16_t m_expectedMaxNumMpdusInPsdu; ///< expected maximum number of MPDUs in PSDUs + + Ptr m_apWifiMac; ///< AP wifi MAC + std::vector> m_stasWifiMac; ///< STAs wifi MAC + Ptr m_apErrorModel; ///< error rate model to corrupt frames sent to the AP + std::vector> + m_errorModels; ///< error rate models to corrupt packets (per STA) + Ptr m_groupcastClient; ///< the packet socket client + + uint16_t m_packets; ///< Number of generated groupcast packets by the application + std::vector + m_phyRxPerSta; ///< count number of PSDUs successfully received by PHY of each STA + uint8_t m_nTxApRts; ///< number of RTS frames sent by the AP + uint8_t m_nTxApCts; ///< number of CTS-to-self frames sent by the AP + std::vector m_txCtsPerSta; ///< count number of CTS responses frames sent by each STA + uint8_t m_totalTx; ///< total number of groupcast frames transmitted by the AP + std::vector> + m_rxGroupcastPerSta; ///< count groupcast packets received by the packet socket server of + ///< each STA and store TX attempt number for each received packet + std::vector m_rxUnicastPerSta; ///< count unicast packets received by the packet + ///< socket server of each STA + + uint8_t m_nTxGroupcastInCurrentTxop; ///< number of groupcast frames transmitted by the AP + ///< (including retries) in the current TXOP + uint8_t + m_nTxRtsInCurrentTxop; ///< number of RTS frames transmitted by the AP in the current TXOP + uint8_t m_nTxCtsInCurrentTxop; ///< number of CTS-to-self frames transmitted by the AP in the + ///< current TXOP + + uint8_t m_nTxAddbaReq; ///< number of transmitted ADDBA Request frames + uint8_t m_nTxAddbaResp; ///< number of transmitted ADDBA Response frames + uint8_t m_nTxDelba; ///< number of transmitted DELBA frames + uint8_t m_nTxGcrAddbaReq; ///< number of transmitted GCR ADDBA Request frames + uint8_t m_nTxGcrAddbaResp; ///< number of transmitted GCR ADDBA Response frames + uint8_t m_nTxGcrDelba; ///< number of transmitted GCR DELBA frames +}; + +/** + * @ingroup wifi-test + * @ingroup tests + * + * @brief Test the implementation of GCR-UR. + * + * GCR-UR tests consider an AP and multiple STAs (with different capabilities) using GCR-UR with up + * to 7 retries. Besides what is verified in the base class, it is checked that: + * + * - When the GCR-UR service is used, each groupcast packet is retransmitted 7 times, in different + * TXOPs + * - When the GCR-UR service is used, all retransmissions of an MPDU have the Retry field in their + * Frame Control fields set to 1 + * - When the GCR-UR service is used, either the initial packet or one of its retransmission is + * successfully received, unless all packets are corrupted + * - When the GCR-UR service is used and MPDU aggregation is enabled, it is checked each STA + * receives the expected amount of MPDUs + * - When the GCR-UR service is used and MPDU aggregation is enabled, it is checked received MPDUs + * are forwarded up in the expected order and the recipient window is correctly flushed + */ +class GcrUrTest : public GcrTestBase +{ + public: + /// Parameters for GCR-UR tests + struct GcrUrParameters + { + uint8_t nGcrRetries{7}; ///< number of solicited retries to use for GCR-UR + uint8_t expectedSkippedRetries{ + 0}; ///< the number of skipped retries because of lifetime expiry + std::optional + packetsPauzeAggregation{}; ///< the amount of generated packets after which MPDU + ///< aggregation should not be used by limiting the queue to + ///< a single packet. If not specified, MPDU aggregation is + ///< not paused + std::optional + packetsResumeAggregation{}; ///< the amount of generated packets after which MPDU + ///< aggregation should be used again by refilling the queue + ///< with more packets. If not specified, MPDU aggregation + ///< is not resumed + }; + + /** + * Constructor + * + * @param testName the name of the test + * @param commonParams the common GCR parameters for the test to run + * @param gcrUrParams the GCR-UR parameters for the test to run + */ + GcrUrTest(const std::string& testName, + const GcrParameters& commonParams, + const GcrUrParameters& gcrUrParams); + ~GcrUrTest() override = default; + + private: + void ConfigureGcrManager(WifiMacHelper& macHelper) override; + void CheckResults() override; + void PacketGenerated(std::string context, Ptr p, const Address& adr) override; + void Transmit(std::string context, + WifiConstPsduMap psduMap, + WifiTxVector txVector, + double txPowerW) override; + void Receive(std::string context, Ptr p, const Address& adr) override; + bool IsUsingAmpduOrSmpdu() const override; + + GcrUrParameters m_gcrUrParams; ///< GCR-UR parameters for the test to run + + std::vector + m_totalTxGroupcasts; ///< total number of groupcast frames transmitted by the AP + ///< (including retries) per original groupcast frame + + Ptr m_currentMpdu; ///< current MPDU + uint64_t m_currentUid; ///< current UID +}; + +/** + * @ingroup wifi-test + * @ingroup tests + * + * @brief wifi GCR Test Suite + */ +class WifiGcrTestSuite : public TestSuite +{ + public: + WifiGcrTestSuite(); +}; + +#endif /* WIFI_GCR_TEST_H */