From cbef286e2d55b47b48a0d9e2b032429485e4970f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:32 +0200 Subject: [PATCH 01/23] physicallayer: fix HT/VHT MCS definitions and guard interval timing Correct HT MCS 32 (1 stream BPSK), MCS 76 (stream 4 16-QAM), and MCS 73 (stream 3 16-QAM) table definitions per IEEE 802.11-2024. Implement data guard-interval queries (getGuardInterval()) and symbol intervals (getSymbolInterval()). Fix HT and VHT signal mode symbol timing to use long symbol duration independently of data GI (Table 19-6 and Table 21-5). Round mixed-format HT and VHT short-GI data airtimes up to 4 us / long-GI symbol boundaries (Eq. 19-90 and Eq. 21-109) while keeping greenfield short-GI data airtimes raw (Eq. 19-92). Extend mode cache keys in Ieee80211HtCompliantModes and Ieee80211VhtCompliantModes with band mode and preamble format to prevent cache key collisions. --- .../Ieee80211LayeredOfdmTransmitter.cc | 3 +- .../wireless/ieee80211/mode/IIeee80211Mode.h | 8 +++ .../ieee80211/mode/Ieee80211HtMode.cc | 61 +++++++++++++++---- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 11 +++- .../ieee80211/mode/Ieee80211OfdmMode.h | 2 +- .../ieee80211/mode/Ieee80211VhtMode.cc | 39 +++++++++--- .../ieee80211/mode/Ieee80211VhtMode.h | 15 ++++- 7 files changed, 109 insertions(+), 30 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc index 30eaa47132d..31d98a3cab4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc @@ -312,7 +312,7 @@ const ITransmission *Ieee80211LayeredOfdmTransmitter::createTransmission(const I // TODO: compute channel const simtime_t preambleDuration = mode->getPreambleLength(); const simtime_t headerDuration = mode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = mode->getDataMode()->getDuration(packet->getDataLength()); + const simtime_t dataDuration = mode->getDataDuration(packet->getDataLength()); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, packetModel, bitModel, symbolModel, sampleModel, analogModel, mode, nullptr); } @@ -331,4 +331,3 @@ Ieee80211LayeredOfdmTransmitter::~Ieee80211LayeredOfdmTransmitter() } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index aa615b7d19e..8fa1b77bbc4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -42,8 +42,13 @@ class INET_API IIeee80211DataMode : public cObject, public IPrintableObject virtual bps getGrossBitrate() const = 0; virtual b getPaddingLength(b dataLength) const = 0; virtual b getCompleteLength(b dataLength) const = 0; + // Returns the raw duration of the encoded data symbol train. PPDU-format + // rules may round this duration at the enclosing mode level. virtual const simtime_t getDuration(b dataLength) const = 0; virtual const simtime_t getSymbolInterval() const = 0; + // Returns the guard interval used by the data symbols, or -1 when the PHY + // has no meaningful guard interval (for example, non-OFDM modes). + virtual const simtime_t getGuardInterval() const { return -1; } virtual const IModulation *getModulation() const = 0; virtual int getNumberOfSpatialStreams() const = 0; }; @@ -68,6 +73,9 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject IIeee80211HeaderMode *_getHeaderMode() const { return const_cast(getHeaderMode()); } IIeee80211DataMode *_getDataMode() const { return const_cast(getDataMode()); } virtual const simtime_t getDuration(b dataLength) const = 0; + virtual const simtime_t getPreambleDuration() const { return getPreambleMode()->getDuration(); } + virtual const simtime_t getHeaderDuration() const { return getHeaderMode()->getDuration(); } + virtual const simtime_t getDataDuration(b dataLength) const { return getDuration(dataLength) - getPreambleDuration() - getHeaderDuration(); } virtual const simtime_t getSlotTime() const = 0; virtual const simtime_t getSifsTime() const = 0; virtual const simtime_t getRifsTime() const = 0; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index 319128cce20..d4498568496 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -161,6 +161,16 @@ unsigned int Ieee80211HtPreambleMode::computeNumberOfHTLongTrainings(unsigned in return numberOfSpaceTimeStreams == 3 ? 4 : numberOfSpaceTimeStreams; } +const simtime_t Ieee80211HtPreambleMode::getDurationBeforeHeader() const +{ + if (preambleFormat == HT_PREAMBLE_MIXED) + return getNonHTShortTrainingSequenceDuration() + getNonHTLongTrainingFieldDuration() + legacySignalMode->getDuration(); + else if (preambleFormat == HT_PREAMBLE_GREENFIELD) + return getHTGreenfieldShortTrainingFieldDuration() + getFirstHTLongTrainingFieldDuration(); + else + throw cRuntimeError("Unknown preamble format"); +} + const simtime_t Ieee80211HtPreambleMode::getDuration() const { // 20.3.7 Mathematical description of signals @@ -178,12 +188,8 @@ const simtime_t Ieee80211HtPreambleMode::getDuration() const bps Ieee80211HtSignalMode::computeGrossBitrate() const { unsigned int numberOfCodedBitsPerSymbol = modulation->getSubcarrierModulation()->getCodeWordSize() * getNumberOfDataSubcarriers(); - if (guardIntervalType == HT_GUARD_INTERVAL_LONG) - return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); - else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) - return bps(numberOfCodedBitsPerSymbol / getShortGISymbolInterval()); - else - throw cRuntimeError("Unknown guard interval type"); + // IEEE Std 802.11-2024, 19.3.11.11.6: the short GI applies only to the Data field. + return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); } bps Ieee80211HtSignalMode::computeNetBitrate() const @@ -239,6 +245,16 @@ bps Ieee80211HtModeBase::getGrossBitrate() const return grossBitrate; } +const simtime_t Ieee80211HtDataMode::getGuardInterval() const +{ + if (guardIntervalType == HT_GUARD_INTERVAL_LONG) + return getGIDuration(); + else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) + return getShortGIDuration(); + else + throw cRuntimeError("Unknown guard interval type"); +} + int Ieee80211HtModeBase::getNumberOfDataSubcarriers() const { return Ieee80211Htmcs::getNumberOfDataSubcarriers(bandwidth, mcsIndex); @@ -285,6 +301,22 @@ const simtime_t Ieee80211HtDataMode::getDuration(b dataLength) const return numberOfSymbols * getSymbolInterval(); } +const simtime_t Ieee80211HtMode::getDuration(b dataLength) const +{ + auto dataDuration = dataMode->getDuration(dataLength); + if (preambleMode->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + { + // IEEE Std 802.11-2024, 19.4.3, Eq. (19-90): mixed-format short-GI + // Data airtime is rounded up to a 4 us boundary. Eq. (19-92) leaves + // greenfield short-GI Data airtime at its raw 3.6 us symbol duration. + auto longGiSymbolInterval = dataMode->getDFTPeriod() + dataMode->getGIDuration(); + auto numberOfLongGiSymbols = (dataDuration.raw() + longGiSymbolInterval.raw() - 1) / longGiSymbolInterval.raw(); + dataDuration = SimTime::fromRaw(numberOfLongGiSymbols * longGiSymbolInterval.raw()); + } + return preambleMode->getDuration() + dataDuration; +} + const simtime_t Ieee80211HtMode::getSlotTime() const { if (centerFrequencyMode == BAND_2_4GHZ) @@ -326,7 +358,7 @@ Ieee80211HtCompliantModes::~Ieee80211HtCompliantModes() const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee80211Htmcs *mcsMode, Ieee80211HtMode::BandMode centerFrequencyMode, Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat, Ieee80211HtModeBase::GuardIntervalType guardIntervalType) { const char *name = ""; // TODO - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType); + auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), centerFrequencyMode, preambleFormat, guardIntervalType); auto mode = singleton.modeCache.find(htModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmModulation *modulation = nullptr; @@ -348,7 +380,7 @@ const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee802 const Ieee80211HtDataMode *dataMode = new Ieee80211HtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211HtPreambleMode *preambleMode = new Ieee80211HtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211HtMode *htMode = new Ieee80211HtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); + singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); return htMode; } return mode->second; @@ -442,7 +474,9 @@ const DI Ieee80211HtmcsTable::htMcs29BW40MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs30BW40MHz([](){ return new Ieee80211Htmcs(30, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs31BW40MHz([](){ return new Ieee80211Htmcs(31, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Ieee80211HtCompliantCodes::htConvolutionalCode5_6, MHz(40));}); -const DI Ieee80211HtmcsTable::htMcs32BW40MHz([](){ return new Ieee80211Htmcs(32, &BpskModulation::singleton, &BpskModulation::singleton, &BpskModulation::singleton, &BpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); +// IEEE Std 802.11-2024, Table 19-35: optional MCS 32 is one BPSK stream. +// This corrects the previous incorrect 4-stream all-BPSK constructor. +const DI Ieee80211HtmcsTable::htMcs32BW40MHz([](){ return new Ieee80211Htmcs(32, &BpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs33BW20MHz([](){ return new Ieee80211Htmcs(33, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs34BW20MHz([](){ return new Ieee80211Htmcs(34, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(20));}); @@ -490,7 +524,9 @@ const DI Ieee80211HtmcsTable::htMcs72BW20MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs73BW20MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs74BW20MHz([](){ return new Ieee80211Htmcs(74, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs75BW20MHz([](){ return new Ieee80211Htmcs(75, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); -const DI Ieee80211HtmcsTable::htMcs76BW20MHz([](){ return new Ieee80211Htmcs(76, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); +// IEEE Std 802.11-2024, Table 19-38 (continued): MCS 76 uses 64-QAM for streams 1-3 and 16-QAM for stream 4. +// This corrects the previous incorrect QPSK modulation for the 4th stream. +const DI Ieee80211HtmcsTable::htMcs76BW20MHz([](){ return new Ieee80211Htmcs(76, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs33BW40MHz([](){ return new Ieee80211Htmcs(33, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs34BW40MHz([](){ return new Ieee80211Htmcs(34, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); @@ -536,7 +572,9 @@ const DI Ieee80211HtmcsTable::htMcs69BW40MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs70BW40MHz([](){ return new Ieee80211Htmcs(70, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs71BW40MHz([](){ return new Ieee80211Htmcs(71, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs72BW40MHz([](){ return new Ieee80211Htmcs(72, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); -const DI Ieee80211HtmcsTable::htMcs73BW40MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); +// IEEE Std 802.11-2024, Table 19-40: MCS 73 uses 64-QAM for streams 1-2 and 16-QAM for stream 3. +// This corrects the previous incorrect 64-QAM modulation for the 3rd stream. +const DI Ieee80211HtmcsTable::htMcs73BW40MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs74BW40MHz([](){ return new Ieee80211Htmcs(74, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs75BW40MHz([](){ return new Ieee80211Htmcs(75, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); @@ -544,4 +582,3 @@ const DI Ieee80211HtmcsTable::htMcs76BW40MHz([](){ return new Ie } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 9184765f083..5e5d43b6fa3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -153,6 +153,7 @@ class INET_API Ieee80211HtPreambleMode : public IIeee80211PreambleMode, public I virtual const simtime_t getSecondAndSubsequentHTLongTrainingFielDuration() const { return 4E-6; } // HT-LTFs, s = 2,3,..,n virtual unsigned int getNumberOfHtLongTrainings() const { return numberOfHTLongTrainings; } + virtual const simtime_t getDurationBeforeHeader() const; virtual const simtime_t getDuration() const override; virtual Ptr createPreamble() const override { return makeShared(); } @@ -218,7 +219,8 @@ class INET_API Ieee80211HtDataMode : public IIeee80211DataMode, public Ieee80211 virtual bps getGrossBitrate() const override { return Ieee80211HtModeBase::getGrossBitrate(); } virtual const Ieee80211Htmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211HtCode *getCode() const { return modulationAndCodingScheme->getCode(); } - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + virtual const simtime_t getGuardInterval() const override; + virtual const simtime_t getSymbolInterval() const override { return getDFTPeriod() + getGuardInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; @@ -263,7 +265,10 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataLength); } + virtual const simtime_t getDuration(b dataLength) const override; + virtual const simtime_t getPreambleDuration() const override { return preambleMode->getDurationBeforeHeader(); } + virtual const simtime_t getHeaderDuration() const override { return preambleMode->getDuration() - getPreambleDuration(); } + virtual const simtime_t getDataDuration(b dataLength) const override { return getDuration(dataLength) - preambleMode->getDuration(); } }; // A specification of the high-throughput (HT) physical layer (PHY) @@ -462,7 +467,7 @@ class INET_API Ieee80211HtCompliantModes protected: static OPP_THREAD_LOCAL const Ieee80211HtCompliantModes singleton; - mutable std::map, const Ieee80211HtMode *> modeCache; + mutable std::map, const Ieee80211HtMode *> modeCache; public: Ieee80211HtCompliantModes(); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h index 6ca84f780f3..608943efba3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h @@ -124,6 +124,7 @@ class INET_API Ieee80211OfdmDataMode : public IIeee80211DataMode, public Ieee802 virtual b getPaddingLength(b dataLength) const override; virtual b getCompleteLength(b dataLength) const override; virtual const simtime_t getDuration(b dataLength) const override; + virtual const simtime_t getGuardInterval() const override { return getGIDuration(); } const Ieee80211OfdmCode *getCode() const { return code; } virtual const simtime_t getSymbolInterval() const override { return Ieee80211OfdmTimingRelatedParametersBase::getSymbolInterval(); } @@ -265,4 +266,3 @@ class INET_API Ieee80211OfdmCompliantModes } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index 89b142e37cb..ea12fd18697 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -241,6 +241,13 @@ unsigned int Ieee80211VhtPreambleMode::computeNumberOfHTLongTrainings(unsigned i return numberOfSpaceTimeStreams == 3 ? 4 : numberOfSpaceTimeStreams; } +const simtime_t Ieee80211VhtPreambleMode::getDurationBeforeHeader() const +{ + // IEEE Std 802.11-2024, 21.3.2: the L-SIG duration is part of the + // pre-header timing of the supported VHT mixed format. + return getNonHTShortTrainingSequenceDuration() + getNonHTLongTrainingFieldDuration() + getLSIGDuration(); +} + const simtime_t Ieee80211VhtPreambleMode::getDuration() const { // 21.3.4 Mathematical description of signals @@ -251,12 +258,9 @@ const simtime_t Ieee80211VhtPreambleMode::getDuration() const bps Ieee80211VhtSignalMode::computeGrossBitrate() const { unsigned int numberOfCodedBitsPerSymbol = modulation->getSubcarrierModulation()->getCodeWordSize() * getNumberOfDataSubcarriers(); - if (guardIntervalType == HT_GUARD_INTERVAL_LONG) - return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); - else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) - return bps(numberOfCodedBitsPerSymbol / getShortGISymbolInterval()); - else - throw cRuntimeError("Unknown guard interval type"); + // IEEE Std 802.11-2024, Table 21-5: VHT-SIG fields use TSYML even + // when the Data field uses short GI; their signaling rate is GI-independent. + return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); } bps Ieee80211VhtSignalMode::computeNetBitrate() const @@ -639,6 +643,20 @@ const simtime_t Ieee80211VhtDataMode::getDuration(b dataLength) const return numberOfSymbols * getSymbolInterval(); } +const simtime_t Ieee80211VhtMode::getDataDuration(b dataBitLength) const +{ + auto dataDuration = dataMode->getDuration(dataBitLength); + if (dataMode->getGuardInterval() == dataMode->getShortGIDuration()) { + // IEEE Std 802.11-2024, 21.4.3, Eq. (21-109): short-GI VHT data + // airtime is the raw TSYMS train rounded up to a TSYML boundary. + // This corrects the previous implementation that used the raw short-GI symbol train. + const auto longGiSymbolInterval = dataMode->getDFTPeriod() + dataMode->getGIDuration(); + const auto numberOfLongGiSymbols = (dataDuration.raw() + longGiSymbolInterval.raw() - 1) / longGiSymbolInterval.raw(); + dataDuration = SimTime::fromRaw(numberOfLongGiSymbols * longGiSymbolInterval.raw()); + } + return dataDuration; +} + const simtime_t Ieee80211VhtMode::getSlotTime() const { if (centerFrequencyMode == BAND_5GHZ) @@ -672,9 +690,13 @@ Ieee80211VhtCompliantModes::~Ieee80211VhtCompliantModes() const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee80211Vhtmcs *mcsMode, Ieee80211VhtMode::BandMode centerFrequencyMode, Ieee80211VhtPreambleMode::HighTroughputPreambleFormat preambleFormat, Ieee80211VhtModeBase::GuardIntervalType guardIntervalType) { + // IEEE Std 802.11-2024, 21.3.2 permits VHT PPDUs only in the mixed + // preamble format represented by this mode implementation. + if (preambleFormat != Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED) + throw cRuntimeError("Unsupported VHT preamble format: only HT_PREAMBLE_MIXED is supported (IEEE Std 802.11-2024, 21.3.2)"); const char *name = ""; // TODO unsigned int nss = mcsMode->getNumNss(); - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss); + auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); auto mode = singleton.modeCache.find(htModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmSignalMode *legacySignal = nullptr; @@ -693,7 +715,7 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 const Ieee80211VhtDataMode *dataMode = new Ieee80211VhtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211VhtPreambleMode *preambleMode = new Ieee80211VhtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211VhtMode *htMode = new Ieee80211VhtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211VhtMode *>(htModeId, htMode)); + singleton.modeCache.insert(std::pair(htModeId, htMode)); return htMode; } return mode->second; @@ -1072,4 +1094,3 @@ const DI Ieee80211VhtmcsTable::vhtMcs9BW160MHzNss8([](){ return } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index aa4aa3639b0..394086ec06f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h @@ -96,6 +96,8 @@ class INET_API Ieee80211VhtSignalMode : public IIeee80211HeaderMode, public Ieee virtual b getLength() const override; virtual bps getNetBitrate() const override { return Ieee80211VhtModeBase::getNetBitrate(); } virtual bps getGrossBitrate() const override { return Ieee80211VhtModeBase::getGrossBitrate(); } + // IEEE Std 802.11-2024, Table 21-5: VHT-SIG uses the long-GI symbol + // interval independently of the data field's selected guard interval. virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulation; } virtual const Ieee80211VhtCode *getCode() const { return code; } @@ -148,6 +150,7 @@ class INET_API Ieee80211VhtPreambleMode : public IIeee80211PreambleMode, public virtual const simtime_t getSecondAndSubsequentHTLongTrainingFielDuration() const { return 4E-6; } // HT-LTFs, s = 2,3,..,n virtual unsigned int getNumberOfHtLongTrainings() const { return numberOfHTLongTrainings; } + virtual const simtime_t getDurationBeforeHeader() const; virtual const simtime_t getDuration() const override; virtual Ptr createPreamble() const override { return makeShared(); } @@ -237,7 +240,10 @@ class INET_API Ieee80211VhtDataMode : public IIeee80211DataMode, public Ieee8021 virtual bps getGrossBitrate() const override { return Ieee80211VhtModeBase::getGrossBitrate(); } virtual const Ieee80211Vhtmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211VhtCode *getCode() const { return modulationAndCodingScheme->getCode(); } - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + virtual const simtime_t getGuardInterval() const override { return guardIntervalType == HT_GUARD_INTERVAL_LONG ? getGIDuration() : getShortGIDuration(); } + // IEEE Std 802.11-2024, Tables 21-5 and 21-8: the VHT Data symbol + // interval is TSYML for long GI and TSYMS for short GI. + virtual const simtime_t getSymbolInterval() const override { return getDFTPeriod() + getGuardInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; @@ -280,7 +286,10 @@ class INET_API Ieee80211VhtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataBitLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataBitLength); } + virtual const simtime_t getDuration(b dataBitLength) const override { return preambleMode->getDuration() + getDataDuration(dataBitLength); } + virtual const simtime_t getPreambleDuration() const override { return preambleMode->getDurationBeforeHeader(); } + virtual const simtime_t getHeaderDuration() const override { return preambleMode->getDuration() - getPreambleDuration(); } + virtual const simtime_t getDataDuration(b dataBitLength) const override; }; // A specification of the high-throughput (HT) physical layer (PHY) @@ -679,7 +688,7 @@ class INET_API Ieee80211VhtCompliantModes protected: static OPP_THREAD_LOCAL const Ieee80211VhtCompliantModes singleton; - mutable std::map, const Ieee80211VhtMode *> modeCache; + mutable std::map, const Ieee80211VhtMode *> modeCache; public: Ieee80211VhtCompliantModes(); From 6de1e5befcc7f6a4d40c5ce07ccf14c0d765705f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:37 +0200 Subject: [PATCH 02/23] physicallayer: complete HT mode set guard interval catalog and lookup Complete Ieee80211ModeSet entries for "n(mixed-2.4Ghz)" with short and long guard-interval variants via completeHtGuardIntervalVariants. Add guard-interval qualified lookups in findMode and getMode. Add findCompatibleMode for exact PHY parameter matching across mode sets (treating negative guard intervals as wildcards for non-OFDM modes). Ensure strict rate monotonicity in getSlowerMode, getFasterMode, getSlowerMandatoryMode, and getFasterMandatoryMode. Implement getMandatoryModeAtOrBelow to find the highest-bitrate mandatory mode at or below a target rate. Add null check in Ieee80211MgmtBase before updating local HT capabilities. Update Ieee80211HtModeSet_1.test and Ieee80211PeerModeSelection_1.test for short-GI mode set awareness. --- .../ieee80211/mode/Ieee80211ModeSet.cc | 125 +++++++++++++----- .../ieee80211/mode/Ieee80211ModeSet.h | 14 +- tests/unit/Ieee80211PeerModeSelection_1.test | 4 + 3 files changed, 108 insertions(+), 35 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 5a53166e2cc..e21dbf9d613 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -578,6 +578,8 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en if (this->referenceMode->getLegacyCwMin() < 0 || this->referenceMode->getLegacyCwMax() < this->referenceMode->getLegacyCwMin()) throw cRuntimeError("Reference mode '%s' in IEEE 802.11 mode set '%s' has invalid contention window bounds", this->referenceMode->getName(), this->name.c_str()); std::vector *nonConstEntries = const_cast *>(&this->entries); + // Keep equal-bitrate modes in declaration order because unqualified lookups + // intentionally preserve the historically preferred mode. std::stable_sort(nonConstEntries->begin(), nonConstEntries->end(), EntryNetBitrateComparator()); // Explicit Supported-Rates eligibility on the authoritative Entry keeps // HT/VHT MCSs out without concrete-type or name-based inference. The @@ -663,20 +665,52 @@ bool Ieee80211ModeSet::getIsMandatory(const IIeee80211Mode *mode) const return entries[getModeIndex(mode)].isMandatory; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode *mode) const { - return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams); + if (mode == nullptr) + return nullptr; + + const auto sourceDataMode = mode->getDataMode(); + const auto sourceBitrate = sourceDataMode->getNetBitrate(); + const auto sourceBandwidth = sourceDataMode->getBandwidth(); + const auto sourceGuardInterval = sourceDataMode->getGuardInterval(); + const auto minBitrate = sourceBitrate - Mbps(0.05); + const auto maxBitrate = sourceBitrate + Mbps(0.05); + for (const auto& entry : entries) { + const auto candidateDataMode = entry.mode->getDataMode(); + const auto candidateBandwidth = candidateDataMode->getBandwidth(); + const auto candidateGuardInterval = candidateDataMode->getGuardInterval(); + const bool bandwidthMatches = (std::isnan(sourceBandwidth.get()) && std::isnan(candidateBandwidth.get())) || + (!std::isnan(sourceBandwidth.get()) && !std::isnan(candidateBandwidth.get()) && sourceBandwidth == candidateBandwidth); + // GI = -1 indicates unconstrained guard interval (e.g., non-OFDM modes). + // Treat GI = -1 as matching any candidate GI, and require exact match when both are >= 0. + const bool guardIntervalMatches = (sourceGuardInterval < SIMTIME_ZERO) || (candidateGuardInterval < SIMTIME_ZERO) || + (sourceGuardInterval >= SIMTIME_ZERO && candidateGuardInterval == sourceGuardInterval); + if (minBitrate <= candidateDataMode->getNetBitrate() && candidateDataMode->getNetBitrate() <= maxBitrate && + bandwidthMatches && candidateDataMode->getNumberOfSpatialStreams() == sourceDataMode->getNumberOfSpatialStreams() && + guardIntervalMatches) + return entry.mode; + } + return nullptr; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +{ + return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); +} + +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const { for (size_t index = 0; index < entries.size(); index++) { auto mode = entries[index].mode; auto dataMode = mode->getDataMode(); auto bitrate = dataMode->getNetBitrate(); + bool guardIntervalMatches = guardInterval < SIMTIME_ZERO || + dataMode->getGuardInterval() == guardInterval; if (minBitrate <= bitrate && bitrate <= maxBitrate && (std::isnan(bandwidth.get()) || dataMode->getBandwidth() == bandwidth) && - (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams)) + (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams) && + guardIntervalMatches) { return entries[index].mode; } @@ -684,20 +718,22 @@ const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, return nullptr; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const { - const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams); + const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); if (mode == nullptr) - throw cRuntimeError("Unknown bitrate: %g in operation mode: '%s'", bitrate.get(), getName()); + throw cRuntimeError("Unknown mode for bitrate %g bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", + bitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); else return mode; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const { - const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams); + const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval); if (mode == nullptr) - throw cRuntimeError("Unknown bitrate: (%g - %g) in operation mode: '%s'", minBitrate.get(), maxBitrate.get(), getName()); + throw cRuntimeError("Unknown mode for bitrate range (%g - %g) bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", + minBitrate.get(), maxBitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); else return mode; } @@ -715,19 +751,25 @@ const IIeee80211Mode *Ieee80211ModeSet::getFastestMode() const const IIeee80211Mode *Ieee80211ModeSet::getSlowerMode(const IIeee80211Mode *mode) const { int index = findModeIndex(mode); - if (index > 0) - return entries[index - 1].mode; - else - return nullptr; + if (index > 0) { + auto bitrate = mode->getDataMode()->getNetBitrate(); + for (int i = index - 1; i >= 0; i--) + if (entries[i].mode->getDataMode()->getNetBitrate() < bitrate) + return entries[i].mode; + } + return nullptr; } const IIeee80211Mode *Ieee80211ModeSet::getFasterMode(const IIeee80211Mode *mode) const { int index = findModeIndex(mode); - if (index >= 0 && index < (int)entries.size() - 1) - return entries[index + 1].mode; - else - return nullptr; + if (index >= 0) { + auto bitrate = mode->getDataMode()->getNetBitrate(); + for (size_t i = index + 1; i < entries.size(); i++) + if (entries[i].mode->getDataMode()->getNetBitrate() > bitrate) + return entries[i].mode; + } + return nullptr; } const IIeee80211Mode *Ieee80211ModeSet::getSlowestMandatoryMode() const @@ -758,24 +800,47 @@ const IIeee80211Mode *Ieee80211ModeSet::getFastestLegacyOperationalMode() const return legacyOperationalModes.empty() ? nullptr : legacyOperationalModes.back(); } +const IIeee80211Mode *Ieee80211ModeSet::getMandatoryModeAtOrBelow(const IIeee80211Mode *mode) const +{ + // Returns the highest-bitrate mandatory mode whose bitrate is <= the given mode's bitrate. + // For equal-bitrate mandatory modes, returns the first-encountered entry (strict > comparison). + // This may return a different mode object than the input when the input is mandatory and + // shares bitrate with another mandatory mode, but the resulting rate is behavior-equivalent. + const auto bitrate = mode->getDataMode()->getNetBitrate(); + const IIeee80211Mode *result = nullptr; + for (const auto& entry : entries) { + const auto entryBitrate = entry.mode->getDataMode()->getNetBitrate(); + if (entry.isMandatory && entryBitrate <= bitrate && + (result == nullptr || entryBitrate > result->getDataMode()->getNetBitrate())) + result = entry.mode; + } + return result; +} + const IIeee80211Mode *Ieee80211ModeSet::getSlowerMandatoryMode(const IIeee80211Mode *mode) const { - int index = findModeIndex(mode); - if (index > 0) - for (int i = index - 1; i >= 0; i--) - if (entries[i].isMandatory) - return entries[i].mode; - return nullptr; + const auto bitrate = mode->getDataMode()->getNetBitrate(); + const IIeee80211Mode *result = nullptr; + for (const auto& entry : entries) { + const auto entryBitrate = entry.mode->getDataMode()->getNetBitrate(); + if (entry.isMandatory && entryBitrate < bitrate && + (result == nullptr || entryBitrate > result->getDataMode()->getNetBitrate())) + result = entry.mode; + } + return result; } const IIeee80211Mode *Ieee80211ModeSet::getFasterMandatoryMode(const IIeee80211Mode *mode) const { - int index = findModeIndex(mode); - if (index >= 0) - for (size_t i = index + 1; i < entries.size(); i++) - if (entries[i].isMandatory) - return entries[i].mode; - return nullptr; + const auto bitrate = mode->getDataMode()->getNetBitrate(); + const IIeee80211Mode *result = nullptr; + for (const auto& entry : entries) { + const auto entryBitrate = entry.mode->getDataMode()->getNetBitrate(); + if (entry.isMandatory && entryBitrate > bitrate && + (result == nullptr || entryBitrate < result->getDataMode()->getNetBitrate())) + result = entry.mode; + } + return result; } const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index e14627dbcbe..0e1a3b6254f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -88,17 +88,21 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject bool containsMode(const IIeee80211Mode *mode) const { return findModeIndex(mode) != -1; } bool getIsMandatory(const IIeee80211Mode *mode) const; - - const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; - const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; - const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; - const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; + // Finds a mode with the same PHY tuple as mode. Unlike findMode(), this + // treats an absent guard interval (negative value) as an exact value. + const IIeee80211Mode *findCompatibleMode(const IIeee80211Mode *mode) const; + + const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *getSlowestMode() const; const IIeee80211Mode *getFastestMode() const; const IIeee80211Mode *getSlowerMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getFasterMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getSlowestMandatoryMode() const; const IIeee80211Mode *getFastestMandatoryMode() const; + const IIeee80211Mode *getMandatoryModeAtOrBelow(const IIeee80211Mode *mode) const; const IIeee80211Mode *getSlowerMandatoryMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getFasterMandatoryMode(const IIeee80211Mode *mode) const; diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index b472fd4e291..6d2b7fbe770 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -134,6 +134,10 @@ const auto *shortGiFallback = selectPeerCompatibleMode(modeSet, &shortGiDisabled ASSERT(shortGiFallback == mcs8Long); ASSERT(!shortGiFallback->isHtShortGuardInterval()); +auto shortGiDisabledOnlyMcs0Peer = makePeerState({0}, {MHz(20)}, MHz(20)); +const auto *shortGiMcs0Fallback = selectPeerCompatibleMode(modeSet, &shortGiDisabledOnlyMcs0Peer, mcs8Short, peer); +ASSERT(shortGiMcs0Fallback == mcs0Long); + auto compatiblePeer = makePeerState({2}, {MHz(20)}, MHz(40)); ASSERT(selectPeerCompatibleMode(modeSet, &compatiblePeer, mcs2Long, peer) == mcs2Long); From a978c0b6cc5597c3f6fe45d45ec824ab1a04b3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:43 +0200 Subject: [PATCH 03/23] physicallayer: support atomic radio and transmitter mode switching Document modeSet precedence over opMode in Ieee80211ConfigureRadioCommand. Implement setModeSetAndMode in Ieee80211Radio and Ieee80211Transmitter to allow atomic reconfiguration of mode set and mode while validating mode membership. Update Ieee80211Transmitter::setModeSet to re-select compatible modes via findCompatibleMode (preserving bitrate, bandwidth, NSS, and GI) or throw when an incompatible transition occurs. In Ieee80211Transmitter::createTransmission, query preamble, header, and data durations directly through IIeee80211Mode methods. --- .../packetlevel/Ieee80211ControlInfo.msg | 6 ++-- .../ieee80211/packetlevel/Ieee80211Radio.cc | 28 +++++++++++++++---- .../ieee80211/packetlevel/Ieee80211Radio.h | 2 +- .../packetlevel/Ieee80211Transmitter.cc | 28 ++++++++++++++----- .../packetlevel/Ieee80211Transmitter.h | 4 +++ 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg index 84d97310414..b819d14e951 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg @@ -17,9 +17,9 @@ namespace inet::physicallayer; // class Ieee80211ConfigureRadioCommand extends ConfigureRadioCommand { - string opMode; // new default operation mode or "" if not set. - const Ieee80211ModeSet *modeSet; // new default mode set or nullptr if not set. - const IIeee80211Mode *mode; // new default transmission mode or nullptr if not set. + string opMode; // new default operation mode or "" if not set; ignored when modeSet is set. + const Ieee80211ModeSet *modeSet; // new default mode set or nullptr if not set; takes precedence over opMode. + const IIeee80211Mode *mode; // new default transmission mode or nullptr if not set; atomically validated against the resolved mode set. IIeee80211Band *band; // new default band or nullptr if not set. Ieee80211Channel *channel; // new default band and channel or nullptr if not set. int channelNumber = -1; // new default channel number in the range [0, numChannels] or -1 if not set. diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 56114b6c2c6..d95d17a929d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -58,13 +58,17 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) Ieee80211ConfigureRadioCommand *configureCommand = dynamic_cast(message->getControlInfo()); if (configureCommand != nullptr) { const char *opMode = configureCommand->getOpMode(); - if (*opMode) - setModeSet(Ieee80211ModeSet::getModeSet(opMode)); const Ieee80211ModeSet *modeSet = configureCommand->getModeSet(); - if (modeSet != nullptr) - setModeSet(modeSet); + // NOTE: When both modeSet and opMode are present, modeSet takes precedence + // and opMode is silently ignored. This differs from the previous behavior + // where both were applied sequentially (with modeSet as final state). + const Ieee80211ModeSet *newModeSet = modeSet != nullptr ? modeSet : (*opMode ? Ieee80211ModeSet::getModeSet(opMode) : nullptr); const IIeee80211Mode *mode = configureCommand->getMode(); - if (mode != nullptr) + if (newModeSet != nullptr && mode != nullptr) + setModeSetAndMode(newModeSet, mode); + else if (newModeSet != nullptr) + setModeSet(newModeSet); + else if (mode != nullptr) setMode(mode); const IIeee80211Band *band = configureCommand->getBand(); if (band != nullptr) @@ -91,6 +95,19 @@ void Ieee80211Radio::setModeSet(const Ieee80211ModeSet *modeSet) emit(listeningChangedSignal, 0); } +void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) + throw cRuntimeError("Invalid mode"); + Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); + Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); + ieee80211Transmitter->setModeSetAndMode(modeSet, mode); + ieee80211Receiver->setModeSet(modeSet); + EV << "Changing radio mode set to " << modeSet << " and mode to " << mode << endl; + receptionTimer = nullptr; + emit(listeningChangedSignal, 0); +} + void Ieee80211Radio::setMode(const IIeee80211Mode *mode) { Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); @@ -334,4 +351,3 @@ const Ptr Ieee80211Radio::peekIeee80211PhyHeaderAtFron } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index aa416de6ed8..874a7890319 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -47,6 +47,7 @@ class INET_API Ieee80211Radio : public FlatRadioBase Ieee80211Radio(); virtual void setModeSet(const Ieee80211ModeSet *modeSet); + virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); @@ -57,4 +58,3 @@ class INET_API Ieee80211Radio : public FlatRadioBase } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 6c76381a790..7011071f1d5 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -75,16 +75,31 @@ const Ieee80211Channel *Ieee80211Transmitter::computeTransmissionChannel(const P void Ieee80211Transmitter::setModeSet(const Ieee80211ModeSet *modeSet) { if (this->modeSet != modeSet) { + auto newMode = mode; + if (mode != nullptr && modeSet != nullptr && !modeSet->containsMode(mode)) { + newMode = modeSet->findCompatibleMode(mode); + if (newMode == nullptr) + throw cRuntimeError("Cannot map current mode to operation mode '%s' without changing bitrate, bandwidth, spatial streams, or guard interval", modeSet->getName()); + } + else if (modeSet == nullptr) + newMode = nullptr; this->modeSet = modeSet; - if (mode != nullptr) - mode = modeSet != nullptr ? modeSet->getMode(mode->getDataMode()->getNetBitrate()) : nullptr; + mode = newMode; } } +void Ieee80211Transmitter::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) + throw cRuntimeError("Invalid mode"); + this->modeSet = modeSet; + this->mode = mode; +} + void Ieee80211Transmitter::setMode(const IIeee80211Mode *mode) { if (this->mode != mode) { - if (modeSet->findMode(mode->getDataMode()->getNetBitrate(), mode->getDataMode()->getBandwidth()) == nullptr) + if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) throw cRuntimeError("Invalid mode"); this->mode = mode; } @@ -151,9 +166,9 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); - const simtime_t headerDuration = transmissionMode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = duration - headerDuration - preambleDuration; + const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); + const simtime_t headerDuration = transmissionMode->getHeaderDuration(); + const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); auto analogModel = getAnalogModel()->createAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, transmissionBandwidth, transmissionPower); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, nullptr, nullptr, nullptr, nullptr, analogModel, transmissionMode, transmissionChannel); } @@ -161,4 +176,3 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index 05b6f29194c..b013fe888b3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -38,7 +38,11 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase virtual const IIeee80211Mode *computeTransmissionMode(const Packet *packet) const; virtual const Ieee80211Channel *computeTransmissionChannel(const Packet *packet) const; + // Re-selects the current mode only when bitrate, bandwidth, NSS, and GI + // remain compatible. Use setModeSetAndMode for an explicit transition. virtual void setModeSet(const Ieee80211ModeSet *modeSet); + // Applies a mode set and an explicitly selected mode as one validated update. + virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); From 2d9022331364358e44f13e5c19c3de5f22b1d6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:47 +0200 Subject: [PATCH 04/23] linklayer: configure guard interval in rate selection Add dataFrameGuardInterval parameter to RateSelection and QosRateSelection NED and C++ initialization, allowing explicit guard interval qualification when fixed bitrates are configured. In computeResponseAckFrameMode and computeResponseCtsFrameMode, use modeSet->getMandatoryModeAtOrBelow(mode) for proper mandatory fallback. Add Ieee80211HtGuardInterval_1.test covering HT/VHT guard-interval catalog verification, timing calculations, lookups, transmitter mode-set switching, and rate selection mandatory fallback. --- .../mac/rateselection/QosRateSelection.cc | 14 +- .../mac/rateselection/QosRateSelection.ned | 5 +- .../mac/rateselection/RateSelection.cc | 6 +- .../mac/rateselection/RateSelection.ned | 5 +- tests/unit/Ieee80211HtGuardInterval_1.test | 369 ++++++++++++++++++ 5 files changed, 383 insertions(+), 16 deletions(-) create mode 100644 tests/unit/Ieee80211HtGuardInterval_1.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index add5ec700f1..e98e49df9df 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -30,7 +30,7 @@ void QosRateSelection::initialize(int stage) double multicastFrameBitrate = par("multicastFrameBitrate"); multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); @@ -101,10 +101,8 @@ const IIeee80211Mode *QosRateSelection::computeResponseAckFrameMode(Packet *pack ASSERT(modeSet->containsMode(mode)); const IIeee80211Mode *responseMode; if (!responseAckFrameMode) { - if (modeSet->getIsMandatory(mode)) - responseMode = mode; - else if (auto slowerMode = modeSet->getSlowerMandatoryMode(mode)) - responseMode = slowerMode; + if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) + responseMode = mandatoryMode; else throw cRuntimeError("Mandatory mode not found"); } @@ -120,10 +118,8 @@ const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *pack ASSERT(modeSet->containsMode(mode)); const IIeee80211Mode *responseMode; if (!responseCtsFrameMode) { - if (modeSet->getIsMandatory(mode)) - responseMode = mode; - else if (auto slowerMode = modeSet->getSlowerMandatoryMode(mode)) - responseMode = slowerMode; + if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) + responseMode = mandatoryMode; else throw cRuntimeError("Mandatory mode not found"); } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 35bf0e1ace5..88ec4706b60 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -32,8 +32,9 @@ simple QosRateSelection extends SimpleModule double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest - double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Unspecified by default - int dataFrameNumSpatialStreams = default(-1); // Unspecified by default + double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified + int dataFrameNumSpatialStreams = default(-1); // Explicit mode qualifier when dataFrameBitrate is specified + double dataFrameGuardInterval @unit(s) = default(-1s); // Explicit mode qualifier when dataFrameBitrate is specified; negative means unspecified, otherwise it must equal the selected PHY mode's modeled GI // Per-receiver unicast data-frame rate. Keys are peer interface module paths (e.g. // "host1.wlan[0]"), resolved to MAC addresses at run time; values are bitrates (bps). diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 9d28a5142cd..5049ce5a1f4 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -35,7 +35,7 @@ void RateSelection::initialize(int stage) double multicastFrameBitrate = par("multicastFrameBitrate"); multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); @@ -108,7 +108,7 @@ const IIeee80211Mode *RateSelection::computeResponseAckFrameMode(Packet *packet, else { auto mode = getMode(packet, dataOrMgmtHeader); ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getIsMandatory(mode) ? mode : modeSet->getSlowerMandatoryMode(mode); // TODO BSSBasicRateSet + auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); } } @@ -120,7 +120,7 @@ const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, else { auto mode = getMode(packet, rtsFrame); ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getIsMandatory(mode) ? mode : modeSet->getSlowerMandatoryMode(mode); // TODO BSSBasicRateSet + auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); } } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 89f63916421..82938a1df50 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -27,8 +27,9 @@ simple RateSelection extends SimpleModule like IRateSelection double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest - double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Unspecified by default - int dataFrameNumSpatialStreams = default(-1); // Unspecified by default + double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified + int dataFrameNumSpatialStreams = default(-1); // Explicit mode qualifier when dataFrameBitrate is specified + double dataFrameGuardInterval @unit(s) = default(-1s); // Explicit mode qualifier when dataFrameBitrate is specified; negative means unspecified, otherwise it must equal the selected PHY mode's modeled GI // Per-receiver unicast data-frame rate. Keys are peer interface module paths (e.g. // "host1.wlan[0]"), resolved to MAC addresses at run time; values are bitrates (bps). diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test new file mode 100644 index 00000000000..cd4bbc4d2a5 --- /dev/null +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -0,0 +1,369 @@ +%description: +Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and airtime. + +%includes: +#include +#include +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TestIeee80211Transmitter : public Ieee80211Transmitter +{ + public: + const IIeee80211Mode *getSelectedMode() const { return mode; } + const Ieee80211ModeSet *getSelectedModeSet() const { return modeSet; } +}; + +class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase +{ + protected: + virtual void handleTimer(cMessage *frame) override {} + virtual void handleCommand(int msgkind, cObject *ctrl) override {} + virtual void handleAuthenticationFrame(Packet *packet, const Ptr& header) override {} + virtual void handleDeauthenticationFrame(Packet *packet, const Ptr& header) override {} + virtual void handleAssociationRequestFrame(Packet *packet, const Ptr& header) override {} + virtual void handleAssociationResponseFrame(Packet *packet, const Ptr& header) override {} + virtual void handleReassociationRequestFrame(Packet *packet, const Ptr& header) override {} + virtual void handleReassociationResponseFrame(Packet *packet, const Ptr& header) override {} + virtual void handleDisassociationFrame(Packet *packet, const Ptr& header) override {} + virtual void handleBeaconFrame(Packet *packet, const Ptr& header) override {} + virtual void handleProbeRequestFrame(Packet *packet, const Ptr& header) override {} + virtual void handleProbeResponseFrame(Packet *packet, const Ptr& header) override {} + + public: + void applyModeSet(const Ieee80211ModeSet *modeSet) + { + receiveSignal(nullptr, modesetChangedSignal, const_cast(modeSet), nullptr); + } + + const ieee80211::Ieee80211SupportedRatesElement& getSupportedRates() const { return supportedRates; } +}; + +%activity: +// Reject unsupported VHT greenfield before any mixed-format cache request. +bool rejectedVhtGreenfieldBeforeMixed = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (cRuntimeError&) { + rejectedVhtGreenfieldBeforeMixed = true; +} +ASSERT(rejectedVhtGreenfieldBeforeMixed); + +const auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +ASSERT(modeSet->getNumModes() == 135); + +TestIeee80211Mgmt mgmt; +mgmt.applyModeSet(modeSet); +const auto& htSupportedRates = mgmt.getSupportedRates(); +const double expectedHtSupportedRates[] = {1, 2, 5.5, 6, 11, 12, 24}; +ASSERT(htSupportedRates.numRates == 7); +for (int i = 0; i < htSupportedRates.numRates; i++) { + ASSERT(htSupportedRates.rate[i] == expectedHtSupportedRates[i]); + if (i > 0) + ASSERT(htSupportedRates.rate[i - 1] < htSupportedRates.rate[i]); +} + +mgmt.applyModeSet(Ieee80211ModeSet::getModeSet("a")); +const auto& legacySupportedRates = mgmt.getSupportedRates(); +const double expectedLegacySupportedRates[] = {6, 12, 24, 9, 18, 36, 48, 54}; +ASSERT(legacySupportedRates.numRates == 8); +for (int i = 0; i < legacySupportedRates.numRates; i++) { + ASSERT(legacySupportedRates.rate[i] == expectedLegacySupportedRates[i]); + ASSERT(legacySupportedRates.basicRate[i] == (i < 3)); +} + +using Key = std::tuple; +std::map modes; +int mandatoryCount = 0; +for (int index = 0; index < modeSet->getNumModes(); index++) { + auto mode = dynamic_cast(modeSet->getMode(index)); + if (mode == nullptr) + continue; + auto dataMode = mode->getDataMode(); + int bandwidth = dataMode->getBandwidth() == MHz(20) ? 20 : dataMode->getBandwidth() == MHz(40) ? 40 : 0; + int mcs = dataMode->getMcsIndex(); + auto guardIntervalType = dataMode->getGuardIntervalType(); + ASSERT(bandwidth != 0); + ASSERT((bandwidth == 20 || bandwidth == 40) && 0 <= mcs && mcs <= 31); + ASSERT(modes.emplace(Key(bandwidth, mcs, guardIntervalType), mode).second); + + bool mustBeMandatory = bandwidth == 20 && mcs <= 7 && + guardIntervalType == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG; + ASSERT(modeSet->isMandatory(index) == mustBeMandatory); + if (modeSet->isMandatory(index)) + mandatoryCount++; +} +ASSERT(mandatoryCount == 8); + +for (int index = 0; index < modeSet->getNumModes(); index++) { + auto mode = modeSet->getMode(index); + auto slowerMode = modeSet->getSlowerMode(mode); + auto fasterMode = modeSet->getFasterMode(mode); + ASSERT(slowerMode == nullptr || slowerMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMode == nullptr || fasterMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); +} + +for (int bandwidth : {20, 40}) { + for (int mcs = 0; mcs <= 31; mcs++) { + auto longMode = modes.at(Key(bandwidth, mcs, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); + auto shortMode = modes.at(Key(bandwidth, mcs, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); + auto longData = longMode->getDataMode(); + auto shortData = shortMode->getDataMode(); + ASSERT(longData->getGuardInterval() == SimTime(800, SIMTIME_NS)); + ASSERT(shortData->getGuardInterval() == SimTime(400, SIMTIME_NS)); + ASSERT(longData->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(shortData->getSymbolInterval() == SimTime(3600, SIMTIME_NS)); + ASSERT(std::fabs(shortData->getNetBitrate().get() * 9 - longData->getNetBitrate().get() * 10) < 1); + + // IEEE Std 802.11-2024, 19.3.11.11.6: short GI is Data-only. + auto longSignal = longMode->getHeaderMode(); + auto shortSignal = shortMode->getHeaderMode(); + ASSERT(longSignal->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(shortSignal->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(longSignal->getDuration() == SimTime(8, SIMTIME_US)); + ASSERT(shortSignal->getDuration() == SimTime(8, SIMTIME_US)); + ASSERT(longSignal->getNetBitrate() == shortSignal->getNetBitrate()); + ASSERT(longSignal->getGrossBitrate() == shortSignal->getGrossBitrate()); + } +} +ASSERT(modes.find(Key(20, 32, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)) == modes.end()); + +auto mixedLong = modes.at(Key(20, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); +auto mixedShort = modes.at(Key(20, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +ASSERT(mixedLong->getDataMode()->getDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataMode()->getDuration(B(24)) == SimTime(32400, SIMTIME_NS)); +ASSERT(mixedShort->getDataMode()->getDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedLong->getDataMode()->getDuration(B(30)) == SimTime(44, SIMTIME_US)); +ASSERT(mixedShort->getDataMode()->getDuration(B(30)) == SimTime(39600, SIMTIME_NS)); + +// IEEE Std 802.11-2024, 19.4.3: Eq. (19-90) rounds mixed-format +// short-GI Data to 4 us, while Eq. (19-92) keeps greenfield Data raw. +ASSERT(mixedShort->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataDuration(B(30)) == SimTime(40, SIMTIME_US)); + +// Preserve representative historical optional-rate timing and bitrate values. +auto mixedShortMcs8 = modes.at(Key(20, 8, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto mixedShortMcs0Bw40 = modes.at(Key(40, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +ASSERT(std::fabs(mixedShortMcs8->getDataMode()->getNetBitrate().get() - 14.4444444444444e6) < 1); +ASSERT(mixedShortMcs8->getDataMode()->getDuration(B(24)) == SimTime(18, SIMTIME_US)); +ASSERT(mixedShortMcs8->getDataDuration(B(24)) == SimTime(20, SIMTIME_US)); +ASSERT(mixedShortMcs8->getDuration(B(24)) == SimTime(60, SIMTIME_US)); +ASSERT(mixedShortMcs0Bw40->getDataMode()->getNetBitrate() == Mbps(15)); +ASSERT(mixedShortMcs0Bw40->getDataMode()->getDuration(B(24)) == SimTime(14400, SIMTIME_NS)); +ASSERT(mixedShortMcs0Bw40->getDataDuration(B(24)) == SimTime(16, SIMTIME_US)); +ASSERT(mixedShortMcs0Bw40->getDuration(B(24)) == SimTime(52, SIMTIME_US)); +auto greenfieldShort = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldShort->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(greenfieldShort->getDataMode()->getNumberOfSpatialStreams() == 2); +ASSERT(greenfieldShort->getDataDuration(B(24)) == SimTime(18, SIMTIME_US)); +ASSERT(greenfieldShort->getDataDuration(B(30)) == SimTime(21600, SIMTIME_NS)); + +auto mixedLongOneSymbol = modes.at(Key(20, 15, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); +auto mixedShortOneSymbol = modes.at(Key(20, 15, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto greenfieldShortOneSymbol = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +for (auto mode : {mixedLongOneSymbol, mixedShortOneSymbol, greenfieldShortOneSymbol}) { + auto dataDuration = mode->getDataDuration(B(24)); + ASSERT(dataDuration >= SIMTIME_ZERO); + ASSERT(mode->getDuration(B(24)) == mode->getPreambleDuration() + mode->getHeaderDuration() + dataDuration); +} +ASSERT(mixedLongOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(mixedShortOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getDataDuration(B(24)) == SimTime(3600, SIMTIME_NS)); +ASSERT(mixedShortOneSymbol->getPreambleDuration() == SimTime(20, SIMTIME_US)); +ASSERT(mixedShortOneSymbol->getHeaderDuration() == SimTime(20, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getPreambleDuration() == SimTime(16, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getHeaderDuration() == SimTime(12, SIMTIME_US)); + +auto unspecified65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1)); +auto long65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1, SimTime(800, SIMTIME_NS))); +auto short65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1, SimTime(400, SIMTIME_NS))); +ASSERT(unspecified65->getDataMode()->getMcsIndex() == 7); +ASSERT(unspecified65->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(long65->getDataMode()->getMcsIndex() == 7); +ASSERT(short65->getDataMode()->getMcsIndex() == 6); + +TestIeee80211Transmitter transmitter; +transmitter.setModeSet(modeSet); +transmitter.setMode(short65); +ASSERT(transmitter.getSelectedMode() == short65); +bool rejectedModeOutsideSet = false; +try { + transmitter.setMode(greenfieldShortOneSymbol); +} +catch (cRuntimeError&) { + rejectedModeOutsideSet = true; +} +ASSERT(rejectedModeOutsideSet); +ASSERT(transmitter.getSelectedMode() == short65); + +auto unspecified135 = dynamic_cast(modeSet->getMode(Mbps(135), MHz(40), 1)); +ASSERT(unspecified135->getDataMode()->getMcsIndex() == 6); +ASSERT(unspecified135->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(modeSet->findMode(Mbps(65), MHz(20), 1, SimTime(600, SIMTIME_NS)) == nullptr); +const auto legacyOfdmModeSet = Ieee80211ModeSet::getModeSet("a"); +ASSERT(legacyOfdmModeSet->findMode(Mbps(6), Hz(NaN), -1, SimTime(800, SIMTIME_NS)) != nullptr); +ASSERT(legacyOfdmModeSet->findMode(Mbps(6), Hz(NaN), -1, SimTime(400, SIMTIME_NS)) == nullptr); + +// Mode-set remapping preserves the complete modeled PHY tuple. In +// particular, a negative GI is an actual absence for compatibility lookup, +// not findMode()'s public wildcard. +const auto erpModeSet = Ieee80211ModeSet::getModeSet("g(erp)"); +const auto pModeSet = Ieee80211ModeSet::getModeSet("p"); +const auto aMode = legacyOfdmModeSet->getMode(Mbps(6), MHz(20), 1, SimTime(800, SIMTIME_NS)); +const auto erpMode = erpModeSet->findCompatibleMode(aMode); +ASSERT(erpMode != nullptr); +ASSERT(erpMode->getDataMode()->getNetBitrate() == aMode->getDataMode()->getNetBitrate()); +ASSERT(erpMode->getDataMode()->getBandwidth() == aMode->getDataMode()->getBandwidth()); +ASSERT(erpMode->getDataMode()->getGuardInterval() == aMode->getDataMode()->getGuardInterval()); + +const auto dsssModeSet = Ieee80211ModeSet::getModeSet("b"); +const auto mixedModeSet = Ieee80211ModeSet::getModeSet("g(mixed)"); +const auto dsssMode = dsssModeSet->getMode(Mbps(1)); +ASSERT(dsssMode->getDataMode()->getGuardInterval() < SIMTIME_ZERO); +ASSERT(mixedModeSet->findCompatibleMode(dsssMode) != nullptr); + +TestIeee80211Transmitter legacyTransmitter; +legacyTransmitter.setModeSet(legacyOfdmModeSet); +legacyTransmitter.setMode(aMode); +legacyTransmitter.setModeSet(erpModeSet); +ASSERT(legacyTransmitter.getSelectedModeSet() == erpModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == erpMode); +const auto modeBeforeRejectedTransition = legacyTransmitter.getSelectedMode(); +bool rejectedIncompatibleModeSet = false; +try { + legacyTransmitter.setModeSet(pModeSet); +} +catch (cRuntimeError&) { + rejectedIncompatibleModeSet = true; +} +ASSERT(rejectedIncompatibleModeSet); +ASSERT(legacyTransmitter.getSelectedModeSet() == erpModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == modeBeforeRejectedTransition); +const auto pMode = pModeSet->getMode(2); // the declared 6 Mbps 10 MHz mode +legacyTransmitter.setModeSetAndMode(pModeSet, pMode); +ASSERT(legacyTransmitter.getSelectedModeSet() == pModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == pMode); + +Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +bool rejectedVhtGreenfieldAfterMixed = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (cRuntimeError&) { + rejectedVhtGreenfieldAfterMixed = true; +} +ASSERT(rejectedVhtGreenfieldAfterMixed); +const auto vhtModeSet = Ieee80211ModeSet::getModeSet("ac"); +const auto vhtLongMcs0 = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +const auto vhtShortMcs0 = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT); +// IEEE Std 802.11-2024, Tables 21-5/21-8 and 21.4.3, Eqs. (21-109)/(21-110). +ASSERT(vhtLongMcs0->getDataMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getSymbolInterval() == SimTime(3600, SIMTIME_NS)); +ASSERT(vhtLongMcs0->getHeaderMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtShortMcs0->getHeaderMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtLongMcs0->getHeaderMode()->getNetBitrate() == vhtShortMcs0->getHeaderMode()->getNetBitrate()); +ASSERT(vhtLongMcs0->getHeaderMode()->getGrossBitrate() == vhtShortMcs0->getHeaderMode()->getGrossBitrate()); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(24)) == SimTime(32400, SIMTIME_NS)); +ASSERT(vhtShortMcs0->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(30)) == SimTime(39600, SIMTIME_NS)); +ASSERT(vhtShortMcs0->getDataDuration(B(30)) == SimTime(40, SIMTIME_US)); +ASSERT(vhtLongMcs0->getDataMode()->getDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtLongMcs0->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +for (auto vhtMode : {vhtLongMcs0, vhtShortMcs0}) + for (auto dataLength : {B(24), B(27), B(30)}) + ASSERT(vhtMode->getDuration(dataLength) == vhtMode->getPreambleDuration() + vhtMode->getHeaderDuration() + vhtMode->getDataDuration(dataLength)); +bool foundVhtLongGuardInterval = false; +bool foundVhtShortGuardInterval = false; +for (int index = 0; index < vhtModeSet->getNumModes(); index++) { + auto vhtMode = dynamic_cast(vhtModeSet->getMode(index)); + if (vhtMode == nullptr) + continue; + auto vhtDataMode = vhtMode->getDataMode(); + auto guardInterval = vhtDataMode->getGuardInterval(); + auto resolvedMode = vhtModeSet->findMode(vhtDataMode->getNetBitrate(), vhtDataMode->getBandwidth(), vhtDataMode->getNumberOfSpatialStreams(), guardInterval); + ASSERT(resolvedMode != nullptr); + ASSERT(resolvedMode->getDataMode()->getGuardInterval() == guardInterval); + foundVhtLongGuardInterval |= guardInterval == SimTime(800, SIMTIME_NS); + foundVhtShortGuardInterval |= guardInterval == SimTime(400, SIMTIME_NS); +} +ASSERT(foundVhtLongGuardInterval); +ASSERT(foundVhtShortGuardInterval); + +auto vhtOneSymbol = dynamic_cast(vhtModeSet->getFastestMode()); +ASSERT(vhtOneSymbol != nullptr); +ASSERT(vhtOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(vhtOneSymbol->getPreambleDuration() == SimTime(20, SIMTIME_US)); +ASSERT(vhtOneSymbol->getHeaderDuration() >= SIMTIME_ZERO); +ASSERT(vhtOneSymbol->getDuration(B(24)) == vhtOneSymbol->getPreambleDuration() + vhtOneSymbol->getHeaderDuration() + vhtOneSymbol->getDataDuration(B(24))); + +for (const auto candidateSet : {modeSet, vhtModeSet}) { + for (int index = 0; index < candidateSet->getNumModes(); index++) { + auto mode = candidateSet->getMode(index); + auto slowerMode = candidateSet->getSlowerMode(mode); + auto fasterMode = candidateSet->getFasterMode(mode); + auto slowerMandatoryMode = candidateSet->getSlowerMandatoryMode(mode); + auto fasterMandatoryMode = candidateSet->getFasterMandatoryMode(mode); + ASSERT(slowerMode == nullptr || slowerMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMode == nullptr || fasterMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); + ASSERT(slowerMandatoryMode == nullptr || slowerMandatoryMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMandatoryMode == nullptr || fasterMandatoryMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); + } +} + +// The equal-rate mandatory candidate is selected by bitrate, not entry index. +auto equalRateVhtMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss2, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +auto mandatoryAtEqualRate = vhtModeSet->getMandatoryModeAtOrBelow(equalRateVhtMode); +ASSERT(mandatoryAtEqualRate != nullptr); +ASSERT(mandatoryAtEqualRate->getDataMode()->getNetBitrate() == equalRateVhtMode->getDataMode()->getNetBitrate()); +ASSERT(vhtModeSet->getIsMandatory(mandatoryAtEqualRate)); + +transmitter.setModeSetAndMode(vhtModeSet, vhtOneSymbol); +ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); +ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); +bool rejectedCombinedModeUpdate = false; +try { + transmitter.setModeSetAndMode(modeSet, greenfieldShortOneSymbol); +} +catch (cRuntimeError&) { + rejectedCombinedModeUpdate = true; +} +ASSERT(rejectedCombinedModeUpdate); +ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); +ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); + +EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; + +%contains: stdout +HT guard interval catalog, timing, and lookup checks passed. From 046347deb79210b105ea782d33f787d2ec5ce5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:50:04 +0200 Subject: [PATCH 05/23] ieee80211: coordinate dynamic mode switches and enforce SSID bounds - Physicallayer / MAC mode-set synchronization: - Emit `modesetChangedSignal` from `Ieee80211Radio::setModeSet` and `Ieee80211Radio::setModeSetAndMode` to propagate dynamic mode set changes across the containing NIC. - Subscribe `Ieee80211Mac` to `modesetChangedSignal` at `INITSTAGE_LINK_LAYER` and implement `receiveSignal` for `cObject *` to synchronize `modeSet` with the physical layer. - Ensures MAC rate selection, rate control adaptors, channel access contention parameters, and management listeners reflect the current physical mode catalog. - Management frame serializer SSID bounds enforcement: - Enforce IEEE Std 802.11-2024 Clause 9.4.2.2 SSID length bounds (0 to 32 octets) during both serialization and deserialization. - Encapsulate SSID wire codec logic into `writeSsidElement` and `readSsidElement` helpers across Probe Request, Association Request, Reassociation Request, Beacon, and Probe Response frames. - Reject overlength SSIDs and truncated wire streams with `cRuntimeError`. - Rate selection documentation: - Add IEEE Std 802.11-2024 Clause 10.6.5.8 reference comments in `RateSelection` and `QosRateSelection` explaining why `getHtMcsIndex() < 0` gates HT peer filtering and non-HT/VHT modes pass through unchanged until VHT MIB state is supported. - Testing: - Add test cases in `Ieee80211SupportedRates_1.test` for valid SSID bounds (0, 1, 32 octets), 33-octet serialization rejection, and truncated stream rejection. - Add test cases in `Ieee80211HtGuardInterval_1.test` verifying radio mode-set signal publication and subscriber notification. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 9 +++ .../linklayer/ieee80211/mac/Ieee80211Mac.h | 1 + .../mac/rateselection/QosRateSelection.cc | 4 + .../mac/rateselection/RateSelection.cc | 4 + .../mgmt/Ieee80211MgmtFrameSerializer.cc | 50 ++++++------ .../ieee80211/packetlevel/Ieee80211Radio.cc | 5 ++ tests/unit/Ieee80211HtGuardInterval_1.test | 67 ++++++++++++++++ tests/unit/Ieee80211SupportedRates_1.test | 78 +++++++++++++++++++ 8 files changed, 192 insertions(+), 26 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index d87f581231f..a33cec0d236 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -67,6 +67,7 @@ void Ieee80211Mac::initialize(int stage) radioModule->subscribe(IRadio::receptionStateChangedSignal, this); radioModule->subscribe(IRadio::transmissionStateChangedSignal, this); radioModule->subscribe(IRadio::receivedSignalPartChangedSignal, this); + getContainingNicModule(this)->subscribe(modesetChangedSignal, this); radio = check_and_cast(radioModule); ds = check_and_cast(getSubmodule("ds")); rx = check_and_cast(getSubmodule("rx")); @@ -346,6 +347,14 @@ void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, intva } } +void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +{ + Enter_Method("%s", cComponent::getSignalName(signalID)); + + if (signalID == modesetChangedSignal) + modeSet = check_and_cast(obj); +} + void Ieee80211Mac::configureRadioMode(IRadio::RadioMode radioMode) { if (radio->getRadioMode() != radioMode) { diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 1114deaed00..6adc6570762 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -67,6 +67,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual void initializeRadioMode(); virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; using MacProtocolBase::receiveSignal; virtual void configureRadioMode(physicallayer::IRadio::RadioMode radioMode); virtual void configureNetworkInterface() override; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index e98e49df9df..141ed3696c1 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -284,6 +284,10 @@ void QosRateSelection::frameTransmitted(Packet *packet, const PtrgetHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 5049ce5a1f4..30d8e881e8e 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -221,6 +221,10 @@ void RateSelection::emitDatarateSelected(cComponent *emitter, const PtrgetHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 09f3ffbd05b..480c291e11a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -33,8 +33,10 @@ Register_Serializer(Ieee80211ReassociationResponseFrame, Ieee80211MgmtFrameSeria static constexpr uint8_t HT_CAPABILITIES_ELEMENT_ID = 45; static constexpr uint8_t HT_OPERATION_ELEMENT_ID = 61; +static constexpr uint8_t SSID_ELEMENT_ID = 0; static constexpr uint8_t SUPPORTED_RATES_ELEMENT_ID = 1; static constexpr uint8_t EXTENDED_SUPPORTED_RATES_ELEMENT_ID = 50; +static constexpr uint8_t MAX_SSID_LENGTH = 32; static constexpr uint8_t MAX_SUPPORTED_RATES = 8; static constexpr uint16_t MAX_EXTENDED_SUPPORTED_RATES = 255; static constexpr double SUPPORTED_RATE_UNIT = 0.5; @@ -43,6 +45,23 @@ static constexpr uint16_t ASSOCIATION_ID_MARKER = 0xC000; static constexpr uint16_t ASSOCIATION_ID_MASK = 0x3FFF; static constexpr int MAX_LOGICAL_ASSOCIATION_ID = 2007; +static void validateSsidLength(size_t length) +{ + // IEEE Std 802.11-2024, 9.4.2.2: the SSID field contains zero to + // 32 octets. Zero octets indicates the wildcard SSID. + if (length > MAX_SSID_LENGTH) + throw cRuntimeError("Malformed SSID element length: %zu exceeds maximum %d", length, MAX_SSID_LENGTH); +} + +static void writeSsidElement(MemoryOutputStream& stream, const char *SSID) +{ + size_t length = strlen(SSID); + validateSsidLength(length); + stream.writeByte(SSID_ELEMENT_ID); + stream.writeByte(static_cast(length)); + stream.writeBytes(reinterpret_cast(SSID), B(length)); +} + static void validateSupportedRatesCount(int numRates) { // IEEE Std 802.11-2024, 9.4.2.3: the Supported Rates field contains @@ -498,11 +517,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c else if (auto probeRequestFrame = dynamicPtrCast(chunk)) { // type = ST_PROBEREQUEST; // 1 SSID - const char *SSID = probeRequestFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME dummy, what is it? - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, probeRequestFrame->getSSID()); // 2 Supported rates writeSupportedRateElements(stream, probeRequestFrame); writeHtElements(stream, probeRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -519,12 +534,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Current AP address stream.writeMacAddress(reassociationRequestFrame->getCurrentAP()); // 4 SSID - const char *SSID = reassociationRequestFrame->getSSID(); - unsigned int length = strlen(SSID); - // FIXME buffer.writeByte(buf + packetLength, ???); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, reassociationRequestFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, reassociationRequestFrame); writeHtElements(stream, reassociationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -542,11 +552,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 2 Listen interval stream.writeUint16Le(0); // FIXME // 3 SSID - const char *SSID = associationRequestFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME dummy, what is it? - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, associationRequestFrame->getSSID()); // 4 Supported rates writeSupportedRateElements(stream, associationRequestFrame); writeHtElements(stream, associationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -597,11 +603,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Capability stream.writeUint16Le(0); // FIXME set capability // 4 Service Set Identifier (SSID) - const char *SSID = beaconFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, beaconFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, beaconFrame); writeHtElements(stream, beaconFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); @@ -636,11 +638,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Capability stream.writeUint16Le(0); // FIXME // 4 SSID - const char *SSID = probeResponseFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, probeResponseFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, probeResponseFrame); writeHtElements(stream, probeResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index d95d17a929d..93582fa25af 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -9,6 +9,7 @@ #include "inet/common/packet/chunk/BitCountChunk.h" #include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" @@ -93,6 +94,8 @@ void Ieee80211Radio::setModeSet(const Ieee80211ModeSet *modeSet) EV << "Changing radio mode set to " << modeSet << endl; receptionTimer = nullptr; emit(listeningChangedSignal, 0); + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); } void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) @@ -106,6 +109,8 @@ void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const II EV << "Changing radio mode set to " << modeSet << " and mode to " << mode << endl; receptionTimer = nullptr; emit(listeningChangedSignal, 0); + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); } void Ieee80211Radio::setMode(const IIeee80211Mode *mode) diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index cd4bbc4d2a5..42c655a0426 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -11,6 +11,8 @@ Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" %global: @@ -24,6 +26,37 @@ class TestIeee80211Transmitter : public Ieee80211Transmitter const Ieee80211ModeSet *getSelectedModeSet() const { return modeSet; } }; +class TestIeee80211Receiver : public Ieee80211Receiver +{ + public: + const Ieee80211ModeSet *getSelectedModeSet() const { return modeSet; } +}; + +class TestIeee80211Radio : public Ieee80211Radio +{ + public: + void setup(ITransmitter *tx, IReceiver *rx) + { + transmitter = tx; + receiver = rx; + } +}; + +class TestModeSetListener : public cListener +{ + public: + const Ieee80211ModeSet *receivedModeSet = nullptr; + int notifications = 0; + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID == modesetChangedSignal) { + receivedModeSet = dynamic_cast(obj); + notifications++; + } + } +}; + class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase { protected: @@ -363,6 +396,40 @@ ASSERT(rejectedCombinedModeUpdate); ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); +// Verify Ieee80211Radio setModeSet and setModeSetAndMode publish modesetChangedSignal +TestIeee80211Transmitter radioTx; +TestIeee80211Receiver radioRx; +TestIeee80211Radio radio; +radio.setup(&radioTx, &radioRx); + +TestModeSetListener radioListener; +radio.subscribe(modesetChangedSignal, &radioListener); + +radio.setModeSet(vhtModeSet); +ASSERT(radioTx.getSelectedModeSet() == vhtModeSet); +ASSERT(radioRx.getSelectedModeSet() == vhtModeSet); +ASSERT(radioListener.notifications == 1); +ASSERT(radioListener.receivedModeSet == vhtModeSet); + +radio.setModeSetAndMode(modeSet, modeSet->getMode(0)); +ASSERT(radioTx.getSelectedModeSet() == modeSet); +ASSERT(radioTx.getSelectedMode() == modeSet->getMode(0)); +ASSERT(radioRx.getSelectedModeSet() == modeSet); +ASSERT(radioListener.notifications == 2); +ASSERT(radioListener.receivedModeSet == modeSet); + +bool radioRejectedInvalidMode = false; +try { + radio.setModeSetAndMode(modeSet, vhtOneSymbol); +} +catch (cRuntimeError&) { + radioRejectedInvalidMode = true; +} +ASSERT(radioRejectedInvalidMode); +ASSERT(radioTx.getSelectedModeSet() == modeSet); +ASSERT(radioRx.getSelectedModeSet() == modeSet); +ASSERT(radioListener.notifications == 2); + EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; %contains: stdout diff --git a/tests/unit/Ieee80211SupportedRates_1.test b/tests/unit/Ieee80211SupportedRates_1.test index a0e82cec048..e25270baab5 100644 --- a/tests/unit/Ieee80211SupportedRates_1.test +++ b/tests/unit/Ieee80211SupportedRates_1.test @@ -412,6 +412,84 @@ assertTiming(pModeSet, SimTime(32, SIMTIME_US), SimTime(13, SIMTIME_US), SimTime assertTiming(nModeSet, SimTime(10, SIMTIME_US), SimTime(20, SIMTIME_US), SimTime(24, SIMTIME_US), 15, 1023); assertTiming(acModeSet, SimTime(16, SIMTIME_US), SimTime(9, SIMTIME_US), SimTime(24, SIMTIME_US), 15, 1023); +// IEEE Std 802.11-2024, 9.4.2.2: SSID length verification (0 to 32 octets) +for (size_t len : {size_t(0), size_t(1), size_t(32)}) { + std::string ssid(len, 'x'); + auto probeReq = makeShared(); + probeReq->setSSID(ssid.c_str()); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1.0; + rates.basicRate[0] = true; + probeReq->setSupportedRates(rates); + probeReq->setChunkLength(B(2 + len + 3)); + + auto bytes = serializeFrame(probeReq); + auto decoded = deserializeFrame(bytes); + ASSERT(std::string(decoded->getSSID()) == ssid); +} + +// 33-octet SSID exceeds standard maximum and must be rejected on serialization +{ + std::string overlengthSsid(33, 'x'); + auto probeReq = makeShared(); + probeReq->setSSID(overlengthSsid.c_str()); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1.0; + rates.basicRate[0] = true; + probeReq->setSupportedRates(rates); + probeReq->setChunkLength(B(2 + 33 + 3)); + + bool overlengthRejected = false; + try { + serializeFrame(probeReq); + } + catch (const cRuntimeError&) { + overlengthRejected = true; + } + ASSERT(overlengthRejected); +} + +// Deserializing a frame with element length = 33 must throw +{ + std::vector rawProbeReq = { + 0x00, 33 // Element 0 (SSID), length 33 + }; + for (int i = 0; i < 33; i++) + rawProbeReq.push_back('x'); + rawProbeReq.push_back(0x01); // Element 1 (Supported Rates) + rawProbeReq.push_back(1); // Length 1 + rawProbeReq.push_back(0x82); // 1.0 Mbps basic + + bool deserializationRejected = false; + try { + deserializeFrame(rawProbeReq); + } + catch (const cRuntimeError&) { + deserializationRejected = true; + } + ASSERT(deserializationRejected); +} + +// Deserializing a truncated SSID element must throw +{ + std::vector truncatedProbeReq = { + 0x00, 10 // Element 0 (SSID), length 10 but only 3 payload bytes follow + }; + for (int i = 0; i < 3; i++) + truncatedProbeReq.push_back('a'); + + bool truncatedRejected = false; + try { + deserializeFrame(truncatedProbeReq); + } + catch (const cRuntimeError&) { + truncatedRejected = true; + } + ASSERT(truncatedRejected); +} + EV << "Supported Rates and AID validation checks passed.\n"; %contains: stdout From ae2f0d0bc20e4846acc099e3f4ec28d7017775c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 00:09:29 +0200 Subject: [PATCH 06/23] linklayer: rebuild configured fixed modes on dynamic mode-set changes - Rate selection dynamic mode-set synchronization: - Factor out fixed mode lookup and mandatory rate computation into updateModes() in RateSelection and QosRateSelection. - Rebuild all configured fixed mode pointers (multicastFrameMode, dataFrameMode, mgmtFrameMode, controlFrameMode, responseAckFrameMode, responseCtsFrameMode, and responseBlockAckFrameMode) from their NED parameters when modesetChangedSignal arrives, preserving data mode bandwidth, spatial-stream, and guard-interval qualifiers. - Refresh fastestMandatoryMode from the new mode set and clear lastTransmittedFrameMode peer rate history to prevent stale mode pointer reuse across mode-set transitions. - Invoke updateModes() on both INITSTAGE_LINK_LAYER initialization and modesetChangedSignal reception. - Testing: - Add unit test cases in Ieee80211HtGuardInterval_1.test verifying that dynamic switching between mode sets (g(erp) and n(mixed-2.4Ghz)) updates all configured mode pointers and fastest mandatory modes in RateSelection and QosRateSelection to reference the active catalog. --- .../mac/rateselection/QosRateSelection.cc | 43 ++++--- .../mac/rateselection/QosRateSelection.h | 1 + .../mac/rateselection/RateSelection.cc | 40 +++--- .../mac/rateselection/RateSelection.h | 1 + tests/unit/Ieee80211HtGuardInterval_1.test | 115 ++++++++++++++++++ 5 files changed, 169 insertions(+), 31 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 141ed3696c1..602d4acda25 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -27,20 +27,7 @@ void QosRateSelection::initialize(int stage) mib.reference(this, "mibModule", true); if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); - double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); - double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); - double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); - double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); - double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); - double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - responseBlockAckFrameMode = (responseBlockAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); - double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); + updateModes(); } } @@ -56,7 +43,7 @@ void QosRateSelection::ensurePerReceiverModesResolved() throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); auto networkInterface = check_and_cast(module); try { - auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; } catch (const cRuntimeError& e) { @@ -65,6 +52,30 @@ void QosRateSelection::ensurePerReceiverModesResolved() } } +void QosRateSelection::updateModes() +{ + if (modeSet == nullptr) + return; + double multicastFrameBitrate = par("multicastFrameBitrate"); + multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + double dataFrameBitrate = par("dataFrameBitrate"); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + double mgmtFrameBitrate = par("mgmtFrameBitrate"); + mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + double controlFrameBitrate = par("controlFrameBitrate"); + controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + double responseAckFrameBitrate = par("responseAckFrameBitrate"); + responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); + responseBlockAckFrameMode = (responseBlockAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); + responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); + fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + lastTransmittedFrameMode.clear(); + perReceiverDataFrameMode.clear(); + perReceiverResolved = false; +} + const IIeee80211Mode *QosRateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -272,7 +283,7 @@ void QosRateSelection::receiveSignal(cComponent *source, simsignal_t signalID, c if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + updateModes(); } } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 569ef742d37..6d503834797 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -59,6 +59,7 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + virtual void updateModes(); // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 30d8e881e8e..8a437bde89f 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -32,19 +32,7 @@ void RateSelection::initialize(int stage) } else if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); - double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); - double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); - double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); - double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); - double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); - double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + updateModes(); // WATCH(dataOrMgmtRateControl); // WATCH(*((cObject**)&fastestMandatoryMode)); @@ -75,7 +63,7 @@ void RateSelection::ensurePerReceiverModesResolved() throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); auto networkInterface = check_and_cast(module); try { - auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; } catch (const cRuntimeError& e) { @@ -84,6 +72,28 @@ void RateSelection::ensurePerReceiverModesResolved() } } +void RateSelection::updateModes() +{ + if (modeSet == nullptr) + return; + double multicastFrameBitrate = par("multicastFrameBitrate"); + multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + double dataFrameBitrate = par("dataFrameBitrate"); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + double mgmtFrameBitrate = par("mgmtFrameBitrate"); + mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + double controlFrameBitrate = par("controlFrameBitrate"); + controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + double responseAckFrameBitrate = par("responseAckFrameBitrate"); + responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); + responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); + fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + lastTransmittedFrameMode.clear(); + perReceiverDataFrameMode.clear(); + perReceiverResolved = false; +} + const IIeee80211Mode *RateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -190,7 +200,7 @@ void RateSelection::receiveSignal(cComponent *source, simsignal_t signalID, cObj if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + updateModes(); } } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 69441331453..7909f9f22e6 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -59,6 +59,7 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + virtual void updateModes(); // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 42c655a0426..3fb4ee8a5bc 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -7,6 +7,8 @@ Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and #include #include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" @@ -57,6 +59,34 @@ class TestModeSetListener : public cListener } }; +class TestRateSelectionAccessor : public ieee80211::RateSelection +{ + public: + static const IIeee80211Mode *getDataFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->dataFrameMode; } + static const IIeee80211Mode *getMulticastFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->multicastFrameMode; } + static const IIeee80211Mode *getMgmtFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->mgmtFrameMode; } + static const IIeee80211Mode *getControlFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->controlFrameMode; } + static const IIeee80211Mode *getResponseAckFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseAckFrameMode; } + static const IIeee80211Mode *getResponseCtsFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseCtsFrameMode; } + static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } + static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } +}; + +class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection +{ + public: + static const IIeee80211Mode *getDataFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->dataFrameMode; } + static const IIeee80211Mode *getResponseBlockAckFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->responseBlockAckFrameMode; } + static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } + static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } +}; + class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase { protected: @@ -430,6 +460,91 @@ ASSERT(radioTx.getSelectedModeSet() == modeSet); ASSERT(radioRx.getSelectedModeSet() == modeSet); ASSERT(radioListener.notifications == 2); +// Verify RateSelection and QosRateSelection dynamic mode-set updates +auto *rateSelection = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.RateSelection")->create("rateSelection", this)); +rateSelection->par("rateControlModule").setStringValue(""); +rateSelection->par("mibModule").setStringValue(""); +rateSelection->par("dataFrameBitrate").setDoubleValue(54e6); +rateSelection->par("multicastFrameBitrate").setDoubleValue(6e6); +rateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); +rateSelection->par("controlFrameBitrate").setDoubleValue(6e6); +rateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); +rateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelection->finalizeParameters(); +rateSelection->buildInside(); + +TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection) == erpModeSet->getMode(Mbps(24))); + +// Dynamically switch rateSelection to HT mode set +TestRateSelectionAccessor::applyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); + +// Also verify HT-specific fixed mode configured with bandwidth, stream, and guard interval +auto *rateSelectionHt = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.RateSelection")->create("rateSelectionHt", this)); +rateSelectionHt->par("rateControlModule").setStringValue(""); +rateSelectionHt->par("mibModule").setStringValue(""); +rateSelectionHt->par("dataFrameBitrate").setDoubleValue(65e6); +rateSelectionHt->par("dataFrameBandwidth").setDoubleValue(20e6); +rateSelectionHt->par("dataFrameNumSpatialStreams").setIntValue(1); +rateSelectionHt->par("dataFrameGuardInterval").setDoubleValue(400e-9); +rateSelectionHt->par("multicastFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("mgmtFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("controlFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("responseAckFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->finalizeParameters(); +rateSelectionHt->buildInside(); + +TestRateSelectionAccessor::applyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelectionHt) == modeSet); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt))); +ASSERT(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt) == short65); + +auto *qosRateSelection = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.QosRateSelection")->create("qosRateSelection", this)); +qosRateSelection->par("rateControlModule").setStringValue(""); +qosRateSelection->par("mibModule").setStringValue(""); +qosRateSelection->par("dataFrameBitrate").setDoubleValue(54e6); +qosRateSelection->par("multicastFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("controlFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseBlockAckFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +qosRateSelection->finalizeParameters(); +qosRateSelection->buildInside(); + +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); + +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); + EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; %contains: stdout From 3c56539f0423bc13b6ba20472b3582946d0e6efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 12:46:47 +0200 Subject: [PATCH 07/23] ieee80211: use legacy basic rates for group-addressed frames Mixed HT catalogs place mandatory HT MCS entries above legacy rates, so the fastest mandatory mode makes Beacons invisible to legacy stations. Constrain both rate selectors to mandatory legacy operational modes when the advertised basic legacy set is nonempty, preserving eligible configured rates. Add passive-discovery coverage for legacy stations associating with mixed HT DCF and HCF access points. --- .../Ieee80211PeerModeSelection.cc | 16 ++ .../Ieee80211PeerModeSelection.h | 3 + .../mac/rateselection/QosRateSelection.cc | 51 ++----- .../mac/rateselection/RateSelection.cc | 18 +-- tests/module/Ieee80211MixedHtDiscovery_1.test | 141 ++++++++++++++++++ 5 files changed, 179 insertions(+), 50 deletions(-) create mode 100644 tests/module/Ieee80211MixedHtDiscovery_1.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 6408b443687..048699e5980 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -14,6 +14,22 @@ namespace ieee80211 { using namespace inet::physicallayer; +const IIeee80211Mode *selectGroupAddressedMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *requestedMode) +{ + // IEEE Std 802.11-2024, 10.6.5.1 and 10.6.5.4. The model advertises + // mandatory legacy operational modes as its BSS basic legacy rate set. + const IIeee80211Mode *legacyMode = nullptr; + for (const auto *candidate : modeSet->getLegacyOperationalModes()) { + if (!modeSet->getIsMandatory(candidate)) + continue; + if (candidate == requestedMode) + return candidate; + if (legacyMode == nullptr || candidate->getDataMode()->getNetBitrate() > legacyMode->getDataMode()->getNetBitrate()) + legacyMode = candidate; + } + return legacyMode != nullptr ? legacyMode : requestedMode; +} + namespace { static const IIeee80211Mode *getLegacyFallback(const Ieee80211ModeSet *modeSet, diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index ba5eaebf764..e89cf445dd8 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -14,6 +14,9 @@ namespace inet { namespace ieee80211 { +INET_API const physicallayer::IIeee80211Mode *selectGroupAddressedMode( + const physicallayer::Ieee80211ModeSet *modeSet, const physicallayer::IIeee80211Mode *requestedMode); + /** * Selects a mode that is compatible with the negotiated receive capabilities * of a peer. Non-HT modes are returned unchanged. A null peer state denotes diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 602d4acda25..7803a558bcc 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -157,9 +157,15 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { + if (dataOrMgmtHeader->getReceiverAddress().isMulticast()) { + const auto *requestedMode = multicastFrameMode; + if (requestedMode == nullptr) + requestedMode = dynamicPtrCast(dataOrMgmtHeader) ? dataFrameMode : mgmtFrameMode; + return selectGroupAddressedMode(modeSet, requestedMode != nullptr ? requestedMode : fastestMandatoryMode); + } // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). // Wins over the interface-wide dataFrameMode / rate control; group-addressed and management - // frames are left to the existing rules below. + // frames were handled above. if (dynamicPtrCast(dataOrMgmtHeader) && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) { ensurePerReceiverModesResolved(); auto it = perReceiverDataFrameMode.find(dataOrMgmtHeader->getReceiverAddress()); @@ -170,46 +176,9 @@ const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const PtrgetReceiverAddress(), dataFrameMode); if (dynamicPtrCast(dataOrMgmtHeader) && mgmtFrameMode) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), mgmtFrameMode); - // This subclause describes the rate selection rules for group addressed data and management frames, excluding - // the following: - // — Non-STBC Beacon and non-STBC PSMP frames - // — STBC group addressed data and management frames - // — Data frames located in an FMS stream (see 10.23.7) - if (dataOrMgmtHeader->getReceiverAddress().isMulticast()) { - // If the BSSBasicRateSet parameter is not empty, a data or management frame (excluding the frames listed - // above) with a group address in the Address 1 field shall be transmitted in a non-HT PPDU using one of the - // rates included in the BSSBasicRateSet parameter or the rate chosen by the AP, described in 10.23.7, if the data - // frames are part of an FMS stream. - // TODO BSSBasicRateSet - // If the BSSBasicRateSet parameter is empty and the BSSBasicMCSSet parameter is not empty, the frame shall - // be transmitted in an HT PPDU using one of the MCSs included in the BSSBasicMCSSet parameter. - - // If both the BSSBasicRateSet parameter and the BSSBasicMCSSet parameter are empty (e.g., a scanning STA - // that is not yet associated with a BSS), the frame shall be transmitted in a non-HT PPDU using one of the - // mandatory PHY rates. - // The rate control is not consulted for these frames. It adapts to the feedback of one - // peer, and a group-addressed frame has no peer: it is never acknowledged, so nothing - // would ever correct a rate chosen for it. - return fastestMandatoryMode; - } - // A data or management frame not identified in 9.7.5.1 through 9.7.5.5 shall be sent using any data rate or MCS - // subject to the following constraints: - // — A STA shall not transmit a frame using a rate or MCS that is not supported by the receiver STA or - // STAs, as reported in any Supported Rates element, Extended Supported Rates element, or - // Supported MCS field in management frames transmitted by the receiver STA. - // — A STA shall not transmit a frame using a value for the CH_BANDWIDTH parameter of the - // TXVECTOR that is not supported by the receiver STA. - // — A STA shall not initiate transmission of a frame at a data rate higher than the greatest rate in the - // OperationalRateSet or the HTOperationalMCSset, which are parameters of the MLME- - // JOIN.request primitive. - else { - // TODO Supported Rates element, Extended Supported Rates element - // TODO OperationalRateSet or the HTOperationalMCSset - if (dataOrMgmtRateControl) - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress())); - else - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); - } + if (dataOrMgmtRateControl) + return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress())); + return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); } const IIeee80211Mode *QosRateSelection::computeControlFrameMode(const Ptr& header, TxopProcedure *txopProcedure) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 8a437bde89f..fe6e062ec7a 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -150,28 +150,28 @@ const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, // const IIeee80211Mode *RateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { + if (dataOrMgmtHeader->getReceiverAddress().isMulticast()) { + const auto *requestedMode = multicastFrameMode; + if (requestedMode == nullptr) + requestedMode = dynamicPtrCast(dataOrMgmtHeader) ? dataFrameMode : mgmtFrameMode; + return selectGroupAddressedMode(modeSet, requestedMode != nullptr ? requestedMode : fastestMandatoryMode); + } // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). // Wins over the interface-wide dataFrameMode / rate control; group-addressed and management - // frames are left to the existing rules below. + // frames were handled above. if (dynamicPtrCast(dataOrMgmtHeader) && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) { ensurePerReceiverModesResolved(); auto it = perReceiverDataFrameMode.find(dataOrMgmtHeader->getReceiverAddress()); if (it != perReceiverDataFrameMode.end()) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), it->second); } - if (dataOrMgmtHeader->getReceiverAddress().isMulticast() && multicastFrameMode) - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), multicastFrameMode); if (dynamicPtrCast(dataOrMgmtHeader) && dataFrameMode) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataFrameMode); if (dynamicPtrCast(dataOrMgmtHeader) && mgmtFrameMode) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), mgmtFrameMode); - // Rate control adapts to the feedback of one peer, and a group-addressed frame has no peer: - // it is never acknowledged, so nothing would ever correct a rate chosen for it. Group-addressed - // frames therefore take a mandatory rate, as the clause above requires. - if (dataOrMgmtRateControl && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) + if (dataOrMgmtRateControl) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress())); - else - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); + return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); } // 802.11-1999 Std. diff --git a/tests/module/Ieee80211MixedHtDiscovery_1.test b/tests/module/Ieee80211MixedHtDiscovery_1.test new file mode 100644 index 00000000000..ae7c47c571b --- /dev/null +++ b/tests/module/Ieee80211MixedHtDiscovery_1.test @@ -0,0 +1,141 @@ +%description: +Two mixed HT APs (DCF and HCF) advertise legacy basic-rate Beacons over the +radio medium. Legacy STAs discover them by passive scanning and associate. +The observation includes selected transmit modes, actual Beacon reception, +and terminal association state, so an HT Beacon cannot pass this test. + +%file: Ieee80211MixedHtDiscovery.cc +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class DiscoveryMac : public Ieee80211Mac +{ + public: + int beaconsSent = 0; + int beaconsReceived = 0; + protected: + virtual void sendDownFrame(Packet *packet) override + { + const auto& header = packet->peekAtFront(); + if (header->getType() == ST_BEACON) { + auto mode = packet->getTag()->getMode(); + ASSERT(mode->getHtMcsIndex() < 0); + ASSERT(modeSet->containsMode(mode)); + ASSERT(modeSet->getIsMandatory(mode)); + beaconsSent++; + } + Ieee80211Mac::sendDownFrame(packet); + } + virtual void handleLowerPacket(Packet *packet) override + { + const auto& header = packet->peekAtFront(); + if (header->getType() == ST_BEACON) { + ASSERT(packet->getTag()->getMode()->getHtMcsIndex() < 0); + beaconsReceived++; + } + Ieee80211Mac::handleLowerPacket(packet); + } +}; +Define_Module(DiscoveryMac); + +class Ieee80211MixedHtDiscoveryTest : public cSimpleModule +{ + protected: + virtual void finish() override + { + for (int i = 0; i < 2; i++) { + auto apNic = getParentModule()->getSubmodule("ap", i)->getSubmodule("wlan", 0); + auto staNic = getParentModule()->getSubmodule("sta", i)->getSubmodule("wlan", 0); + auto ap = check_and_cast(apNic->getSubmodule("mib")); + auto sta = check_and_cast(staNic->getSubmodule("mib")); + ASSERT(ap->isHtOperationSupported()); + ASSERT(!sta->isHtOperationSupported()); + ASSERT(sta->bssStationData.isAssociated); + ASSERT(sta->bssData.bssid == ap->address); + ASSERT(ap->bssAccessPointData.stations.at(sta->address) == Ieee80211Mib::ASSOCIATED); + ASSERT(ap->findPeerHtState(sta->address) == nullptr); + ASSERT(sta->findPeerHtState(ap->address) == nullptr); + ASSERT(check_and_cast(apNic->getSubmodule("mac"))->beaconsSent > 0); + ASSERT(check_and_cast(staNic->getSubmodule("mac"))->beaconsReceived > 0); + } + std::cout << "Legacy passive discovery and association with mixed HT DCF and HCF APs verified.\n"; + } +}; +Define_Module(Ieee80211MixedHtDiscoveryTest); + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module DiscoveryMac extends Ieee80211Mac +{ + parameters: + @class(::DiscoveryMac); +} + +simple Ieee80211MixedHtDiscoveryTest extends SimpleModule +{ + parameters: + @class(::Ieee80211MixedHtDiscoveryTest); +} + +network DiscoveryNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap[2]: AccessPoint; + sta[2]: WirelessHost; + test: Ieee80211MixedHtDiscoveryTest; +} + +%inifile: omnetpp.ini +[General] +network = DiscoveryNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +*.ap[*].wlan[*].opMode = "n(mixed-2.4Ghz)" +*.sta[*].wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = -1bps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 6 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB +**.wlan[*].mac.typename = "DiscoveryMac" +*.ap[0].wlan[0].mac.qosStation = false +*.ap[1].wlan[0].mac.qosStation = true +*.ap[*].wlan[0].mgmt.typename = "Ieee80211MgmtAp" +*.ap[0].wlan[0].mgmt.ssid = "mixed-dcf" +*.ap[1].wlan[0].mgmt.ssid = "mixed-hcf" +*.ap[*].wlan[0].mgmt.beaconInterval = 5ms +*.sta[*].wlan[0].mgmt.typename = "Ieee80211MgmtSta" +*.sta[*].wlan[0].mgmt.numChannels = 1 +*.sta[*].wlan[0].agent.typename = "Ieee80211AgentSta" +*.sta[*].wlan[0].agent.activeScan = false +*.sta[0].wlan[0].agent.startingTime = 0s +*.sta[1].wlan[0].agent.startingTime = 1ms +*.sta[0].wlan[0].agent.defaultSsid = "mixed-dcf" +*.sta[1].wlan[0].agent.defaultSsid = "mixed-hcf" +*.sta[*].wlan[0].agent.channelsToScan = "6" +*.sta[*].wlan[0].agent.minChannelTime = 10ms +*.sta[*].wlan[0].agent.maxChannelTime = 10ms +*.sta[*].wlan[0].agent.authenticationTimeout = 20ms +*.sta[*].wlan[0].agent.associationTimeout = 20ms + +%contains: stdout +Legacy passive discovery and association with mixed HT DCF and HCF APs verified. From 243341566d5e67615d39d83ada28cc240481624b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 12:46:47 +0200 Subject: [PATCH 08/23] ieee80211: make radio mode-set transitions transactional A failed mode-set listener could leave the radio and an arbitrary prefix of consumers on a new catalog. Coordinate dependent updates through a typed transaction interface and restore their snapshots if an update or synchronous notification fails. Reuse the MAC's antenna- and channel-width-limited HT capability derivation during transitions, completing MIB and peer updates before notifying observers. Preserve transmitter-owned compatible-mode mapping and reject reentrant catalog changes. Cover HT-to-legacy and legacy-to-HT transitions, Beacon advertisements, both selectors, fixed-rate rejection through both radio setters, and observer-failure rollback. Rename existing unit-test helpers to avoid collisions with the transaction API. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 55 +++- .../linklayer/ieee80211/mac/Ieee80211Mac.h | 7 +- .../ieee80211/mac/channelaccess/Dcaf.cc | 23 +- .../ieee80211/mac/channelaccess/Dcaf.h | 5 +- .../ieee80211/mac/channelaccess/Edcaf.cc | 23 +- .../ieee80211/mac/channelaccess/Edcaf.h | 4 + .../ieee80211/mac/common/ModeSetListener.cc | 20 +- .../ieee80211/mac/common/ModeSetListener.h | 8 +- .../mac/ratecontrol/AarfRateControl.cc | 10 + .../mac/ratecontrol/AarfRateControl.h | 1 + .../mac/ratecontrol/OnoeRateControl.cc | 10 + .../mac/ratecontrol/OnoeRateControl.h | 1 + .../mac/ratecontrol/RateControlBase.cc | 14 +- .../mac/ratecontrol/RateControlBase.h | 1 + .../mac/rateselection/QosRateSelection.cc | 28 +- .../mac/rateselection/QosRateSelection.h | 5 +- .../mac/rateselection/RateSelection.cc | 28 +- .../mac/rateselection/RateSelection.h | 8 +- .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 66 ++-- .../ieee80211/mgmt/Ieee80211MgmtBase.h | 8 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 10 +- .../linklayer/ieee80211/mib/Ieee80211Mib.h | 3 + .../packetlevel/IIeee80211ModeSetListener.h | 39 +++ .../ieee80211/packetlevel/Ieee80211Radio.cc | 79 ++++- .../ieee80211/packetlevel/Ieee80211Radio.h | 6 + .../ieee80211/packetlevel/Ieee80211Receiver.h | 1 + .../packetlevel/Ieee80211Transmitter.h | 2 + .../module/Ieee80211ModeSetTransition_1.test | 287 ++++++++++++++++++ tests/unit/Ieee80211HtGuardInterval_1.test | 34 +-- 29 files changed, 682 insertions(+), 104 deletions(-) create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h create mode 100644 tests/module/Ieee80211ModeSetTransition_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index a33cec0d236..c5b7502c9a0 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include + #include #include "inet/common/INETUtils.h" @@ -72,20 +74,7 @@ void Ieee80211Mac::initialize(int stage) ds = check_and_cast(getSubmodule("ds")); rx = check_and_cast(getSubmodule("rx")); tx = check_and_cast(getSubmodule("tx")); - int operationalHtSpatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), - modeSet->getMaximumNumberOfSpatialStreams()); - std::set operationalChannelWidths; - if (modeSet->isHtOperationSupported()) { - const auto *transmitter = dynamic_cast(radio->getTransmitter()); - const auto *receiver = dynamic_cast(radio->getReceiver()); - if (transmitter == nullptr || receiver == nullptr) - throw cRuntimeError("HT operation requires Ieee80211Transmitter and Ieee80211Receiver"); - for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) - if (transmitter->isHtChannelWidthSupported(channelWidth) && - receiver->isHtChannelWidthSupported(channelWidth)) - operationalChannelWidths.insert(channelWidth); - } - mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); + updateLocalHtCapabilities(); emit(modesetChangedSignal, modeSet); if (isUp()) initializeRadioMode(); @@ -98,6 +87,24 @@ void Ieee80211Mac::initialize(int stage) } } +void Ieee80211Mac::updateLocalHtCapabilities() +{ + int operationalHtSpatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), + modeSet->getMaximumNumberOfSpatialStreams()); + std::set operationalChannelWidths; + if (modeSet->isHtOperationSupported()) { + const auto *transmitter = dynamic_cast(radio->getTransmitter()); + const auto *receiver = dynamic_cast(radio->getReceiver()); + if (transmitter == nullptr || receiver == nullptr) + throw cRuntimeError("HT operation requires Ieee80211Transmitter and Ieee80211Receiver"); + for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) + if (transmitter->isHtChannelWidthSupported(channelWidth) && + receiver->isHtChannelWidthSupported(channelWidth)) + operationalChannelWidths.insert(channelWidth); + } + mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); +} + void Ieee80211Mac::initializeRadioMode() { const char *initialRadioMode = par("initialRadioMode"); @@ -350,9 +357,25 @@ void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, intva void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) - modeSet = check_and_cast(obj); +void Ieee80211Mac::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateLocalHtCapabilities(); +} + +std::function Ieee80211Mac::saveModeSetState() +{ + Enter_Method_Silent(); + auto restoreMib = mib->saveHtState(); + return [this, state = std::make_tuple(modeSet), restoreMib]() mutable { + std::tie(modeSet) = std::move(state); + restoreMib(); + }; } void Ieee80211Mac::configureRadioMode(IRadio::RadioMode radioMode) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 6adc6570762..3fb5c2ec704 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -21,6 +21,7 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Pcf.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { namespace ieee80211 { @@ -35,10 +36,13 @@ class Ieee80211MacHeader; * exact operation of the MAC depend on the plugged-in components (see IUpperMac, * IRx, ITx, IContention and other interface classes). */ -class INET_API Ieee80211Mac : public MacProtocolBase +class INET_API Ieee80211Mac : public MacProtocolBase, public physicallayer::IIeee80211ModeSetListener { public: static simsignal_t frameTransmissionOutcomeSignal; + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; protected: FcsMode fcsMode; @@ -65,6 +69,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; virtual void initializeRadioMode(); + void updateLocalHtCapabilities(); virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 1f6ea0615cb..ec065a96cb6 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/contract/IRx.h" @@ -125,13 +127,26 @@ void Dcaf::expectedChannelAccess(simtime_t time) void Dcaf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } +void Dcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + calculateTimingParameters(); } +std::function Dcaf::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax)]() mutable { + std::tie(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax) = std::move(state); + }; +} + + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h index 2fab0d312c8..eb15a2dbb7d 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h @@ -19,8 +19,11 @@ namespace ieee80211 { class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener { + public: + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: - physicallayer::Ieee80211ModeSet *modeSet = nullptr; IContention *contention = nullptr; IChannelAccess::ICallback *callback = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index e220635a876..0d8dff39627 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -189,13 +191,26 @@ int Edcaf::getCwMin(AccessCategory ac, int aCwMin) void Edcaf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } +void Edcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + calculateTimingParameters(); } +std::function Edcaf::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax)]() mutable { + std::tie(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax) = std::move(state); + }; +} + + } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index e737cad9162..8e25569caa0 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h @@ -30,6 +30,10 @@ namespace ieee80211 { */ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener { + public: + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IContention *contention = nullptr; IChannelAccess::ICallback *callback = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc index 15dd3838eb5..f84574d5447 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -23,11 +25,25 @@ void ModeSetListener::initialize(int stage) void ModeSetListener::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) - modeSet = check_and_cast(obj); +void ModeSetListener::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); } +std::function ModeSetListener::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet)]() mutable { + std::tie(modeSet) = std::move(state); + }; +} + + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h index 05581613050..0a2ecc63001 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h @@ -10,12 +10,18 @@ #include "inet/common/SimpleModule.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { namespace ieee80211 { -class INET_API ModeSetListener : public SimpleModule, public cListener +class INET_API ModeSetListener : public SimpleModule, public cListener, public physicallayer::IIeee80211ModeSetListener { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: physicallayer::Ieee80211ModeSet *modeSet = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc index 0b2e47b79f7..f43245df0b0 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc @@ -14,6 +14,16 @@ using namespace inet::physicallayer; Define_Module(AarfRateControl); +std::function AarfRateControl::saveModeSetState() +{ + Enter_Method_Silent(); + auto restoreBase = ModeSetListener::saveModeSetState(); + return [this, savedStations = stations, restoreBase]() mutable { + stations.swap(savedStations); + restoreBase(); + }; +} + void AarfRateControl::initialize(int stage) { RateControlBase::initialize(stage); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h index f9ddd7f5222..1fa323b11f5 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h @@ -50,6 +50,7 @@ class INET_API AarfRateControl : public RateControlBase virtual void increaseRateIfTimerIsExpired(State& state); public: + virtual std::function saveModeSetState() override; virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) override; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; virtual void frameReceived(Packet *frame) override; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index a49f35d0c30..be2acddff39 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -14,6 +14,16 @@ using namespace inet::physicallayer; Define_Module(OnoeRateControl); +std::function OnoeRateControl::saveModeSetState() +{ + Enter_Method_Silent(); + auto restoreBase = ModeSetListener::saveModeSetState(); + return [this, savedStations = stations, restoreBase]() mutable { + stations.swap(savedStations); + restoreBase(); + }; +} + void OnoeRateControl::initialize(int stage) { RateControlBase::initialize(stage); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index ecee1d849e4..5995abb8c61 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -48,6 +48,7 @@ class INET_API OnoeRateControl : public RateControlBase virtual void computeModeIfTimerIsExpired(State& state); public: + virtual std::function saveModeSetState() override; virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) override; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; virtual void frameReceived(Packet *frame) override; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index 551a31c4c9e..e91291f31ed 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -64,10 +64,16 @@ void RateControlBase::receiveSignal(cComponent *source, simsignal_t signalID, cO { Enter_Method("%s", cComponent::getSignalName(signalID)); - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - resetRateControl(); - } + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} + +void RateControlBase::applyModeSet(const Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + getInitialMode(); // Validate fixed initial rates even before the first peer is used. + resetRateControl(); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index 2a61b5028fa..9ceb1e18d54 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -18,6 +18,7 @@ namespace ieee80211 { class INET_API RateControlBase : public ModeSetListener, public IRateControl { public: + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; static simsignal_t datarateChangedSignal; protected: diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 7803a558bcc..7aa9b3aaec3 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" @@ -249,11 +251,29 @@ const IIeee80211Mode *QosRateSelection::computeMode(Packet *packet, const Ptr(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - updateModes(); - } +void QosRateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); +} + +std::function QosRateSelection::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, responseBlockAckFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved)]() mutable { + std::tie(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, responseBlockAckFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved) = std::move(state); + }; } void QosRateSelection::frameTransmitted(Packet *packet, const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 6d503834797..53cb75b94e1 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -32,11 +32,14 @@ namespace ieee80211 { */ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListener { + public: + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; - const physicallayer::Ieee80211ModeSet *modeSet = nullptr; std::map lastTransmittedFrameMode; // originator frame modes diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index fe6e062ec7a..e2ef635a78e 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" @@ -197,11 +199,29 @@ const IIeee80211Mode *RateSelection::computeMode(Packet *packet, const Ptr(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - updateModes(); - } +void RateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); +} + +std::function RateSelection::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved)]() mutable { + std::tie(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved) = std::move(state); + }; } void RateSelection::frameTransmitted(Packet *packet, const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 7909f9f22e6..32294eae93c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { namespace ieee80211 { @@ -31,8 +32,13 @@ namespace ieee80211 { * 9.7.6.4 Rate selection for control frames that are not control response frames * 9.7.6.5 Rate selection for control response frames */ -class INET_API RateSelection : public IRateSelection, public SimpleModule, public cListener // FIXME +class INET_API RateSelection : public IRateSelection, public SimpleModule, public cListener, public physicallayer::IIeee80211ModeSetListener // FIXME { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 3c3bc33903a..acf01610c92 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h" +#include + #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" #include "inet/common/ProtocolTag_m.h" @@ -43,35 +45,47 @@ void Ieee80211MgmtBase::initialize(int stage) void Ieee80211MgmtBase::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - supportedRates = Ieee80211SupportedRatesElement(); - extendedSupportedRates = Ieee80211ExtendedSupportedRatesElement(); - int rateIndex = 0; - int extendedRateIndex = 0; - // Supported Rates carries the legacy OperationalRateSet only. HT/VHT - // MCS support is advertised through the corresponding capabilities - // elements (IEEE Std 802.11-2024, 9.4.2.3, 9.4.2.54.4, 11.1.4.6). - for (const auto *mode : modeSet->getLegacyOperationalModes()) { - bool isBasicRate = modeSet->getIsMandatory(mode); - double rate = mode->getDataMode()->getNetBitrate().get(); - if (rateIndex < 8) { - supportedRates.rate[rateIndex] = rate; - supportedRates.basicRate[rateIndex] = isBasicRate; - rateIndex++; - } - else if (extendedRateIndex < 255) { - extendedSupportedRates.rate[extendedRateIndex] = rate; - extendedSupportedRates.basicRate[extendedRateIndex] = isBasicRate; - extendedRateIndex++; - } - else - throw cRuntimeError("Mode set '%s' contains more than 263 legacy operational rates", modeSet->getName()); +void Ieee80211MgmtBase::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + supportedRates = Ieee80211SupportedRatesElement(); + extendedSupportedRates = Ieee80211ExtendedSupportedRatesElement(); + int rateIndex = 0; + int extendedRateIndex = 0; + // Supported Rates carries the legacy OperationalRateSet only. HT/VHT + // MCS support is advertised through the corresponding capabilities + // elements (IEEE Std 802.11-2024, 9.4.2.3, 9.4.2.54.4, 11.1.4.6). + for (const auto *mode : modeSet->getLegacyOperationalModes()) { + bool isBasicRate = modeSet->getIsMandatory(mode); + double rate = mode->getDataMode()->getNetBitrate().get(); + if (rateIndex < 8) { + supportedRates.rate[rateIndex] = rate; + supportedRates.basicRate[rateIndex] = isBasicRate; + rateIndex++; + } + else if (extendedRateIndex < 255) { + extendedSupportedRates.rate[extendedRateIndex] = rate; + extendedSupportedRates.basicRate[extendedRateIndex] = isBasicRate; + extendedRateIndex++; } - supportedRates.numRates = rateIndex; - extendedSupportedRates.numRates = extendedRateIndex; + else + throw cRuntimeError("Mode set '%s' contains more than 263 legacy operational rates", modeSet->getName()); } + supportedRates.numRates = rateIndex; + extendedSupportedRates.numRates = extendedRateIndex; +} + +std::function Ieee80211MgmtBase::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, supportedRates, extendedSupportedRates)]() mutable { + std::tie(modeSet, supportedRates, extendedSupportedRates) = std::move(state); + }; } void Ieee80211MgmtBase::addHtCapabilities(const Ptr& frame) const diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index 145da89e902..79c41175a8a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -19,6 +19,7 @@ #include "inet/networklayer/contract/IInterfaceTable.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { @@ -28,8 +29,13 @@ namespace ieee80211 { * Abstract base class for 802.11 infrastructure mode management components. * */ -class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener +class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener, public physicallayer::IIeee80211ModeSetListener { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: // configuration ModuleRefByPar mib; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index e5c8fd008a3..0d28b31a9c8 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -7,8 +7,9 @@ #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" -#include +#include +#include #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" @@ -112,6 +113,13 @@ const Ieee80211HtOperation& Ieee80211Mib::getHtOperation() const return htOperation; } +std::function Ieee80211Mib::saveHtState() +{ + return [this, state = std::make_tuple(localHtCapabilitiesValid, localHtCapabilities, htOperation, configuredSecondaryChannelOffset, primaryChannelAvailable, peerHtStates)]() mutable { + std::tie(localHtCapabilitiesValid, localHtCapabilities, htOperation, configuredSecondaryChannelOffset, primaryChannelAvailable, peerHtStates) = std::move(state); + }; +} + void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit) { diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index 691257c2f07..9432c7a003e 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211MIB_H #define __INET_IEEE80211MIB_H +#include + #include "inet/common/SimpleModule.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" @@ -100,6 +102,7 @@ class INET_API Ieee80211Mib : public SimpleModule short allocateAssociationId(const MacAddress& address); void releaseAssociationId(const MacAddress& address); void clearAssociationIds(); + std::function saveHtState(); void updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit); bool isHtOperationSupported() const { return localHtCapabilitiesValid; } diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h new file mode 100644 index 00000000000..d714adf87aa --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h @@ -0,0 +1,39 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IIEEE80211MODESETLISTENER_H +#define __INET_IIEEE80211MODESETLISTENER_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +class Ieee80211ModeSet; + +/** + * Transaction participant for a radio's mode-set changes. Stateful subscribers + * to modesetChanged must implement this role to support recoverable changes. + * The radio captures every participant before applying any change, applies all + * participants before publishing the signal, and restores them if a call fails. + * Snapshot callbacks are one-shot, must not throw or emit signals, and restore + * all state affected by applyModeSet. Applying a change must not notify observers. + */ +class INET_API IIeee80211ModeSetListener +{ + public: + virtual ~IIeee80211ModeSetListener() = default; + virtual const Ieee80211ModeSet *getModeSet() const = 0; + virtual std::function saveModeSetState() = 0; + virtual void applyModeSet(const Ieee80211ModeSet *modeSet) = 0; +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 93582fa25af..b0abedb5211 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -7,6 +7,10 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include + +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" + #include "inet/common/packet/chunk/BitCountChunk.h" #include "inet/common/ProtocolTag_m.h" #include "inet/common/Simsignals.h" @@ -87,30 +91,73 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) void Ieee80211Radio::setModeSet(const Ieee80211ModeSet *modeSet) { - Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); - Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); - ieee80211Transmitter->setModeSet(modeSet); - ieee80211Receiver->setModeSet(modeSet); - EV << "Changing radio mode set to " << modeSet << endl; - receptionTimer = nullptr; - emit(listeningChangedSignal, 0); - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); + Enter_Method("setModeSet"); + changeModeSet(modeSet, nullptr, false); } void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) { + Enter_Method("setModeSetAndMode"); + changeModeSet(modeSet, mode, true); +} + +void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode) +{ + if (changingModeSet) + throw cRuntimeError("Reentrant radio mode-set change"); if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) throw cRuntimeError("Invalid mode"); - Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); - Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); - ieee80211Transmitter->setModeSetAndMode(modeSet, mode); - ieee80211Receiver->setModeSet(modeSet); - EV << "Changing radio mode set to " << modeSet << " and mode to " << mode << endl; + auto transmitter = const_cast(check_and_cast(this->transmitter)); + auto receiver = const_cast(check_and_cast(this->receiver)); + const auto *oldTransmitterModeSet = transmitter->getModeSet(); + const auto *oldReceiverModeSet = receiver->getModeSet(); + const auto *oldMode = transmitter->getMode(); + auto oldReceptionTimer = receptionTimer; + + // Discover the same subscribers that receive the hierarchical notification, + // deduplicating participants subscribed at more than one level. Capture all + // state first: a failing participant may have partially changed itself. + std::vector participants; + std::vector> restore; + for (cComponent *component = this; component != nullptr; component = component->getParentModule()) { + for (auto listener : component->getLocalSignalListeners(modesetChangedSignal)) { + auto participant = dynamic_cast(listener); + if (participant != nullptr && std::find(participants.begin(), participants.end(), participant) == participants.end()) + participants.push_back(participant); + } + } + if (modeSet == nullptr && !participants.empty()) + throw cRuntimeError("Cannot clear the radio mode set while MAC mode-set consumers are attached"); + for (auto participant : participants) + restore.push_back(participant->saveModeSetState()); + + changingModeSet = true; + try { + if (explicitMode) + transmitter->setModeSetAndMode(modeSet, mode); + else + transmitter->setModeSet(modeSet); + receiver->setModeSet(modeSet); + for (auto participant : participants) + participant->applyModeSet(modeSet); + // Every built-in dependent, including the MIB, is now current before + // any observer runs. Built-in signal adapters skip this applied set. + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); + } + catch (...) { + transmitter->setModeSetAndMode(oldTransmitterModeSet, oldMode); + receiver->setModeSet(oldReceiverModeSet); + for (auto it = restore.rbegin(); it != restore.rend(); ++it) + (*it)(); + receptionTimer = oldReceptionTimer; + changingModeSet = false; + throw; + } + changingModeSet = false; + EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; receptionTimer = nullptr; emit(listeningChangedSignal, 0); - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); } void Ieee80211Radio::setMode(const IIeee80211Mode *mode) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 874a7890319..841c6ac2e63 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -30,11 +30,14 @@ class INET_API Ieee80211Radio : public FlatRadioBase static const Ptr peekIeee80211PhyHeaderAtFront(const Packet *packet, b length = b(-1), int flags = 0); protected: + bool changingModeSet = false; FcsMode fcsMode = FCS_MODE_UNDEFINED; protected: virtual void initialize(int stage) override; + void changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode); + virtual void handleUpperCommand(cMessage *message) override; virtual void insertFcs(const Ptr& phyHeader) const; @@ -46,6 +49,9 @@ class INET_API Ieee80211Radio : public FlatRadioBase public: Ieee80211Radio(); + // These setters snapshot transactional mode-set consumers before applying + // the catalog, and restore them if an update or synchronous notification + // throws. Stateful modesetChanged subscribers implement IIeee80211ModeSetListener. virtual void setModeSet(const Ieee80211ModeSet *modeSet); virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h index 450e8b36c45..49de67c6980 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -37,6 +37,7 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override; + const Ieee80211ModeSet *getModeSet() const { return modeSet; } virtual void setModeSet(const Ieee80211ModeSet *modeSet); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index b013fe888b3..3ac8ebcea24 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -40,6 +40,8 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase // Re-selects the current mode only when bitrate, bandwidth, NSS, and GI // remain compatible. Use setModeSetAndMode for an explicit transition. + const Ieee80211ModeSet *getModeSet() const { return modeSet; } + const IIeee80211Mode *getMode() const { return mode; } virtual void setModeSet(const Ieee80211ModeSet *modeSet); // Applies a mode set and an explicitly selected mode as one validated update. virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test new file mode 100644 index 00000000000..6ec9a84d8da --- /dev/null +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -0,0 +1,287 @@ +%description: +Exercise both radio mode-set setters with DCF and HCF selectors. Successful +HT/legacy transitions refresh PHY-limited capabilities and actual Beacon bodies. +Fixed-rate and observer failures restore all subscribed catalogs, caches, peer +negotiations, and channel-access windows. No signal replay is used for rollback. + +%file: Ieee80211ModeSetTransition.cc + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class TransitionMgmtAp : public Ieee80211MgmtAp +{ + bool probing = false; + public: + void checkAdvertisement() + { + Enter_Method_Silent(); + probing = true; + sendBeacon(); + probing = false; + } + protected: + virtual void sendDown(Packet *packet) override + { + if (!probing) { + Ieee80211MgmtAp::sendDown(packet); + return; + } + const auto& body = packet->peekAtFront(); + ASSERT(body->getHtCapabilitiesPresent() == modeSet->isHtOperationSupported()); + ASSERT(body->getHtOperationPresent() == modeSet->isHtOperationSupported()); + const auto& rates = body->getSupportedRates(); + const auto& extended = body->getExtendedSupportedRates(); + const auto& modes = modeSet->getLegacyOperationalModes(); + ASSERT(rates.numRates + extended.numRates == (int)modes.size()); + for (int i = 0; i < (int)modes.size(); i++) { + double rate = i < rates.numRates ? rates.rate[i] : extended.rate[i - rates.numRates]; + bool basic = i < rates.numRates ? rates.basicRate[i] : extended.basicRate[i - rates.numRates]; + ASSERT(rate == modes[i]->getDataMode()->getNetBitrate().get()); + ASSERT(basic == modeSet->getIsMandatory(modes[i])); + } + if (modeSet->isHtOperationSupported()) { + for (int i = 0; i < 77; i++) + ASSERT(body->getHtCapabilities().rxMcsSupported[i] == (i < 8)); + ASSERT(!body->getHtCapabilities().supportedChannelWidth40Mhz); + ASSERT(!body->getHtCapabilities().shortGi40); + } + delete packet; + } +}; +Define_Module(TransitionMgmtAp); + +class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener +{ + bool rejectNotification = false; + int notifications = 0; + MacAddress peer = MacAddress("02:00:00:00:01:ff"); + + std::vector participants(cModule *radio) + { + std::vector result; + for (cComponent *component = radio; component; component = component->getParentModule()) + for (auto listener : component->getLocalSignalListeners(modesetChangedSignal)) + if (auto participant = dynamic_cast(listener)) + result.push_back(participant); + return result; + } + + void check(cModule *nic, const Ieee80211ModeSet *expected, bool peerExpected) + { + auto radio = check_and_cast(nic->getSubmodule("radio")); + ASSERT(check_and_cast(radio->getTransmitter())->getModeSet() == expected); + ASSERT(check_and_cast(radio->getReceiver())->getModeSet() == expected); + for (auto participant : participants(radio)) + ASSERT(participant->getModeSet() == expected); + auto mib = check_and_cast(nic->getSubmodule("mib")); + ASSERT(mib->isHtOperationSupported() == expected->isHtOperationSupported()); + ASSERT(mib->requirePrimaryChannel() == 6); + ASSERT((mib->findPeerHtState(peer) != nullptr) == peerExpected); + for (int i = 0; i < 77; i++) { + ASSERT(mib->localHtCapabilities.rxMcsSupported[i] == (expected->isHtOperationSupported() && i < 8)); + ASSERT(mib->getHtOperation().basicMcsSupported[i] == (expected->isHtOperationSupported() && i < 8)); + } + if (peerExpected) { + auto state = mib->findPeerHtState(peer); + ASSERT(state->negotiatedCapabilities.localTxPeerRx.valid); + for (int i = 8; i < 77; i++) + ASSERT(!state->negotiatedCapabilities.localTxPeerRx.supportedMcs[i]); + } + check_and_cast(nic->getSubmodule("mgmt"))->checkAdvertisement(); + auto data = makeShared(); + auto beacon = makeShared(); + beacon->setType(ST_BEACON); + for (auto address : {MacAddress::BROADCAST_ADDRESS, MacAddress("01:00:5e:00:00:01"), peer}) { + data->setReceiverAddress(address); + beacon->setReceiverAddress(address); + Packet packet("selection probe"); + auto mac = nic->getSubmodule("mac"); + auto dcf = check_and_cast(mac->getSubmodule("dcf")->getSubmodule("rateSelection")); + auto qos = check_and_cast(mac->getSubmodule("hcf")->getSubmodule("rateSelection")); + for (const auto& header : {Ptr(data), Ptr(beacon)}) { + auto nonQosMode = dcf->computeMode(&packet, header); + auto qosMode = qos->computeMode(&packet, header, nullptr); + ASSERT(expected->containsMode(nonQosMode)); + ASSERT(expected->containsMode(qosMode)); + if (address.isMulticast()) { + ASSERT(nonQosMode->getHtMcsIndex() < 0); + ASSERT(qosMode->getHtMcsIndex() < 0); + ASSERT(expected->getIsMandatory(nonQosMode)); + ASSERT(expected->getIsMandatory(qosMode)); + } + } + } + } + + protected: + virtual void initialize() override { scheduleAt(SimTime(1, SIMTIME_US), new cMessage("switch")); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override + { + Enter_Method_Silent(); + notifications++; + auto radio = check_and_cast(source); + auto target = check_and_cast(value); + // Publication must follow *all* internal updates, irrespective of where + // this observer appears in the subscriber list. + auto nic = radio->getParentModule(); + for (auto participant : participants(radio)) + ASSERT(participant->getModeSet() == target); + ASSERT(check_and_cast(nic->getSubmodule("mib"))->isHtOperationSupported() == target->isHtOperationSupported()); + check_and_cast(nic->getSubmodule("mgmt"))->checkAdvertisement(); + if (rejectNotification) + throw cRuntimeError("test observer rejects mode set"); + } + + virtual void handleMessage(cMessage *message) override + { + delete message; + for (int index : {1, 2, 0}) { + auto nic = getParentModule()->getSubmodule("ap", index)->getSubmodule("wlan", 0); + auto radio = check_and_cast(nic->getSubmodule("radio")); + auto mib = check_and_cast(nic->getSubmodule("mib")); + auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); + radio->setMode(ht->getMode(Mbps(24))); + radio->subscribe(modesetChangedSignal, this); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + check(nic, ht, true); + auto oldGeneration = mib->findPeerHtState(peer)->generation; + auto tx = check_and_cast(radio->getTransmitter()); + auto oldMode = tx->getMode(); + auto dcf = nic->getSubmodule("mac")->getSubmodule("dcf"); + auto hcf = nic->getSubmodule("mac")->getSubmodule("hcf"); + auto channelAccess = check_and_cast(dcf->getSubmodule("channelAccess")); + auto edcaf = check_and_cast(hcf->getSubmodule("edca")->getSubmodule("edcaf", 0)); + channelAccess->incrementCw(); + edcaf->incrementCw(); + int oldCw = channelAccess->getCw(); + int oldQosCw = edcaf->getCw(); + // Populate both adaptive histories before any failed transition. + auto arf = check_and_cast(dcf->getSubmodule("rateControl")); + auto onoe = check_and_cast(hcf->getSubmodule("rateControl")); + auto oldArf = arf->getRate(peer); + auto oldOnoe = onoe->getRate(peer); + if (index != 0) { + for (bool explicitMode : {false, true}) { + bool rejected = false; + try { + if (explicitMode) + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + else + radio->setModeSet(legacy); + } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + ASSERT(notifications == 0); + check(nic, ht, true); + ASSERT(tx->getMode() == oldMode); + ASSERT(mib->findPeerHtState(peer)->generation == oldGeneration); + ASSERT(channelAccess->getCw() == oldCw); + ASSERT(edcaf->getCw() == oldQosCw); + ASSERT(arf->getRate(peer) == oldArf); + ASSERT(onoe->getRate(peer) == oldOnoe); + } + radio->unsubscribe(modesetChangedSignal, this); + continue; + } + // Reject after all internal updates have succeeded, to cover arbitrary + // notification-prefix failure in addition to fixed-rate lookup failure. + rejectNotification = true; + bool rejected = false; + try { radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + ASSERT(notifications == 1); + check(nic, ht, true); + ASSERT(mib->findPeerHtState(peer)->generation == oldGeneration); + ASSERT(channelAccess->getCw() == oldCw); + ASSERT(edcaf->getCw() == oldQosCw); + rejectNotification = false; + radio->setModeSet(legacy); + check(nic, legacy, false); + radio->setModeSetAndMode(ht, ht->getMode(Mbps(65), MHz(20), 1, 800e-9)); + check(nic, ht, false); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + check(nic, ht, true); + ASSERT(notifications == 3); + radio->unsubscribe(modesetChangedSignal, this); + } + std::cout << "Mode-set switching, advertisements, peer state, both selectors, and rollback verified.\n"; + endSimulation(); + } +}; +Define_Module(Ieee80211ModeSetTransitionTest); + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.wireless.AccessPoint; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple TransitionMgmtAp extends Ieee80211MgmtAp +{ + parameters: + @class(::TransitionMgmtAp); +} + +simple Ieee80211ModeSetTransitionTest extends SimpleModule +{ + parameters: + @class(::Ieee80211ModeSetTransitionTest); +} + +network TransitionNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap[3]: AccessPoint; + test: Ieee80211ModeSetTransitionTest; +} + +%inifile: omnetpp.ini +[General] +network = TransitionNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = -1bps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 6 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].mac.qosStation = true +*.ap[*].wlan[0].mgmt.typename = "TransitionMgmtAp" +*.ap[*].wlan[0].mgmt.ssid = "transition" +*.ap[*].wlan[0].mgmt.beaconInterval = 1s +*.ap[*].wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.ap[*].wlan[0].mac.hcf.rateControl.typename = "OnoeRateControl" + +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameBitrate = 65Mbps +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameBandwidth = 20MHz +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameNumSpatialStreams = 1 +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameGuardInterval = 800ns +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameGuardInterval = 800ns + +%contains: stdout +Mode-set switching, advertisements, peer state, both selectors, and rollback verified. diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 3fb4ee8a5bc..11e854a6a0c 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -69,8 +69,8 @@ class TestRateSelectionAccessor : public ieee80211::RateSelection static const IIeee80211Mode *getResponseAckFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseAckFrameMode; } static const IIeee80211Mode *getResponseCtsFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseCtsFrameMode; } static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } - static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } - static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { + static const Ieee80211ModeSet *readModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void notifyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } }; @@ -81,8 +81,8 @@ class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection static const IIeee80211Mode *getDataFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->dataFrameMode; } static const IIeee80211Mode *getResponseBlockAckFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->responseBlockAckFrameMode; } static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } - static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } - static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { + static const Ieee80211ModeSet *readModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void notifyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } }; @@ -104,7 +104,7 @@ class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase virtual void handleProbeResponseFrame(Packet *packet, const Ptr& header) override {} public: - void applyModeSet(const Ieee80211ModeSet *modeSet) + void notifyModeSet(const Ieee80211ModeSet *modeSet) { receiveSignal(nullptr, modesetChangedSignal, const_cast(modeSet), nullptr); } @@ -129,7 +129,7 @@ const auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); ASSERT(modeSet->getNumModes() == 135); TestIeee80211Mgmt mgmt; -mgmt.applyModeSet(modeSet); +mgmt.notifyModeSet(modeSet); const auto& htSupportedRates = mgmt.getSupportedRates(); const double expectedHtSupportedRates[] = {1, 2, 5.5, 6, 11, 12, 24}; ASSERT(htSupportedRates.numRates == 7); @@ -139,7 +139,7 @@ for (int i = 0; i < htSupportedRates.numRates; i++) { ASSERT(htSupportedRates.rate[i - 1] < htSupportedRates.rate[i]); } -mgmt.applyModeSet(Ieee80211ModeSet::getModeSet("a")); +mgmt.notifyModeSet(Ieee80211ModeSet::getModeSet("a")); const auto& legacySupportedRates = mgmt.getSupportedRates(); const double expectedLegacySupportedRates[] = {6, 12, 24, 9, 18, 36, 48, 54}; ASSERT(legacySupportedRates.numRates == 8); @@ -473,8 +473,8 @@ rateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); rateSelection->finalizeParameters(); rateSelection->buildInside(); -TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); -ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); +TestRateSelectionAccessor::notifyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); @@ -485,8 +485,8 @@ ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMo ASSERT(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection) == erpModeSet->getMode(Mbps(24))); // Dynamically switch rateSelection to HT mode set -TestRateSelectionAccessor::applyModeSet(rateSelection, modeSet); -ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); +TestRateSelectionAccessor::notifyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); @@ -513,8 +513,8 @@ rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6e6); rateSelectionHt->finalizeParameters(); rateSelectionHt->buildInside(); -TestRateSelectionAccessor::applyModeSet(rateSelectionHt, modeSet); -ASSERT(TestRateSelectionAccessor::getModeSet(rateSelectionHt) == modeSet); +TestRateSelectionAccessor::notifyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::readModeSet(rateSelectionHt) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt))); ASSERT(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt) == short65); @@ -531,14 +531,14 @@ qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); qosRateSelection->finalizeParameters(); qosRateSelection->buildInside(); -TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); -ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); +TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); -TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); -ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); +TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == modeSet); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); From a783eac9be74d670166a2ec9ee1a4d942bb30af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 22:03:46 +0200 Subject: [PATCH 09/23] ieee80211: mark malformed response AIDs incorrect Invalid wire AIDs in association and reassociation responses used to throw during deserialization. Mark the frame incorrect and substitute zero so parsing preserves the status, rates, trailing elements, and stream position. Add raw-byte coverage for missing markers, successful AIDs outside the valid range, and nonzero unsuccessful-response AIDs in both response types. The focused management serializer unit test passes against the rebuilt debug library. --- .../mgmt/Ieee80211MgmtFrameSerializer.cc | 20 ++++----- .../unit/Ieee80211MgmtFrameSerializer_1.test | 43 ++++++++++++++----- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 480c291e11a..72e1d578b46 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -471,18 +471,16 @@ static uint16_t encodeAssociationId(Ieee80211StatusCode statusCode, int aid) return 0; } -static int decodeAssociationId(Ieee80211StatusCode statusCode, uint16_t wireAid) +static int decodeAssociationId(Ieee80211AssociationResponseFrame& frame, uint16_t wireAid) { - if (statusCode == SC_SUCCESSFUL) { - if ((wireAid & ASSOCIATION_ID_MARKER) != ASSOCIATION_ID_MARKER) - throw cRuntimeError("Malformed successful Association Response AID: missing marker 0xC000"); + if (frame.getStatusCode() == SC_SUCCESSFUL) { const int aid = wireAid & ASSOCIATION_ID_MASK; - if (aid < 1 || aid > MAX_LOGICAL_ASSOCIATION_ID) - throw cRuntimeError("Malformed successful Association Response AID: %d", aid); - return aid; + if ((wireAid & ASSOCIATION_ID_MARKER) == ASSOCIATION_ID_MARKER && aid >= 1 && aid <= MAX_LOGICAL_ASSOCIATION_ID) + return aid; + frame.markIncorrect(); } - if (wireAid != 0) - throw cRuntimeError("Malformed unsuccessful Association Response AID: expected zero, got 0x%04x", wireAid); + else if (wireAid != 0) + frame.markIncorrect(); return 0; } @@ -733,7 +731,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre auto frame = makeShared(); stream.readUint16Le(); frame->setStatusCode((Ieee80211StatusCode)stream.readUint16Le()); - frame->setAid(decodeAssociationId(frame->getStatusCode(), stream.readUint16Le())); + frame->setAid(decodeAssociationId(*frame, stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); @@ -745,7 +743,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre auto frame = makeShared(); stream.readUint16Le(); frame->setStatusCode((Ieee80211StatusCode)stream.readUint16Le()); - frame->setAid(decodeAssociationId(frame->getStatusCode(), stream.readUint16Le())); + frame->setAid(decodeAssociationId(*frame, stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); diff --git a/tests/unit/Ieee80211MgmtFrameSerializer_1.test b/tests/unit/Ieee80211MgmtFrameSerializer_1.test index f387d6b4c81..96467753136 100644 --- a/tests/unit/Ieee80211MgmtFrameSerializer_1.test +++ b/tests/unit/Ieee80211MgmtFrameSerializer_1.test @@ -172,6 +172,36 @@ static void checkMalformedSupportedRates(const std::vector& prefix) ASSERT(zeroLength->getSupportedRates().numRates == 0); } +template +static void checkMalformedAssociationIds() +{ + for (uint16_t status : {0, 0x1234}) { + const std::vector invalidAids = status == 0 ? + std::vector{0x0001, 0x4001, 0x8001, 0xC000, 0xC7D8, 0xFFFF} : + std::vector{0x0001, 0xC000, 0xC001}; + for (auto aid : invalidAids) { + auto bytes = appendTrailingInformationElement({ + 0x00, 0x00, uint8_t(status), uint8_t(status >> 8), uint8_t(aid), uint8_t(aid >> 8), + 0x01, 0x01, 0x0C, 0x32, 0x01, 0x18 + }); + auto frame = deserializeMalformedBody(bytes); + MemoryInputStream stream(bytes); + Ieee80211MgmtFrameSerializer serializer; + auto decoded = serializer.deserialize(stream, typeid(T)); + ASSERT(decoded->isIncorrect()); + ASSERT(stream.getPosition() == B(bytes.size())); + ASSERT(frame->isIncorrect()); + ASSERT(frame->isComplete()); + ASSERT(frame->getStatusCode() == status); + ASSERT(frame->getAid() == 0); + ASSERT(frame->getSupportedRates().numRates == 1); + ASSERT(frame->getSupportedRates().rate[0] == 6); + ASSERT(frame->getExtendedSupportedRates().numRates == 1); + ASSERT(frame->getExtendedSupportedRates().rate[0] == 12); + } + } +} + %activity: { @@ -267,17 +297,8 @@ static void checkMalformedSupportedRates(const std::vector& prefix) ASSERT(frame->getAid() == 0x0245); } -bool missingAssociationIdMarkerRejected = false; -try { - deserializeBody({ - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x01, 0x01, 0x0C - }); -} -catch (const cRuntimeError&) { - missingAssociationIdMarkerRejected = true; -} -ASSERT(missingAssociationIdMarkerRejected); +checkMalformedAssociationIds(); +checkMalformedAssociationIds(); bool successfulZeroAssociationIdRejected = false; try { From ff9be1ce8d5ca22afacf305680214e1ad8ef1e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 22:03:58 +0200 Subject: [PATCH 10/23] ieee80211: commit mode-set changes before observer notification An observer exception could roll back a mode set already cached by an earlier listener. Complete the radio and behavioral participant transaction before publishing its result, and propagate listener exceptions without undoing the announced state. Retain the reentrancy guard during publication. Extend the transition test with an ordinary caching listener followed by an ordinary throwing listener. Verify observer and radio consistency after publication failure while retaining DCF and HCF rollback coverage for update failures. The focused transition module test passes against the rebuilt debug library. --- .../ieee80211/packetlevel/Ieee80211Radio.cc | 19 +++++--- .../ieee80211/packetlevel/Ieee80211Radio.h | 5 +- .../module/Ieee80211ModeSetTransition_1.test | 48 ++++++++++++++----- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index b0abedb5211..81e4996c737 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -140,10 +140,6 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 receiver->setModeSet(modeSet); for (auto participant : participants) participant->applyModeSet(modeSet); - // Every built-in dependent, including the MIB, is now current before - // any observer runs. Built-in signal adapters skip this applied set. - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); } catch (...) { transmitter->setModeSetAndMode(oldTransmitterModeSet, oldMode); @@ -154,10 +150,21 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 changingModeSet = false; throw; } + receptionTimer = nullptr; + // The transaction is committed. Observer failures must not undo a state + // already published to earlier listeners. Keep the reentrancy guard during + // publication so every listener observes the same committed mode set. + try { + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); + emit(listeningChangedSignal, 0); + } + catch (...) { + changingModeSet = false; + throw; + } changingModeSet = false; EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; - receptionTimer = nullptr; - emit(listeningChangedSignal, 0); } void Ieee80211Radio::setMode(const IIeee80211Mode *mode) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 841c6ac2e63..976b43b23db 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -50,8 +50,9 @@ class INET_API Ieee80211Radio : public FlatRadioBase Ieee80211Radio(); // These setters snapshot transactional mode-set consumers before applying - // the catalog, and restore them if an update or synchronous notification - // throws. Stateful modesetChanged subscribers implement IIeee80211ModeSetListener. + // the catalog, and restore them if an update throws. Notifications publish + // committed state; listener exceptions propagate without rolling it back. + // Behavioral consumers implement IIeee80211ModeSetListener. virtual void setModeSet(const Ieee80211ModeSet *modeSet); virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test index 6ec9a84d8da..50e9669bf32 100644 --- a/tests/module/Ieee80211ModeSetTransition_1.test +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -1,8 +1,8 @@ %description: Exercise both radio mode-set setters with DCF and HCF selectors. Successful HT/legacy transitions refresh PHY-limited capabilities and actual Beacon bodies. -Fixed-rate and observer failures restore all subscribed catalogs, caches, peer -negotiations, and channel-access windows. No signal replay is used for rollback. +Fixed-rate failures restore all subscribed catalogs, caches, peer negotiations, +and channel-access windows. Observer failures leave published state committed. %file: Ieee80211ModeSetTransition.cc @@ -62,10 +62,24 @@ class TransitionMgmtAp : public Ieee80211MgmtAp }; Define_Module(TransitionMgmtAp); +class CachingModeSetObserver : public cListener +{ + public: + const Ieee80211ModeSet *modeSet = nullptr; + int notifications = 0; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override + { + modeSet = check_and_cast(value); + notifications++; + } +}; + class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener { bool rejectNotification = false; int notifications = 0; + const Ieee80211ModeSet *observedModeSet = nullptr; + CachingModeSetObserver cachingObserver; MacAddress peer = MacAddress("02:00:00:00:01:ff"); std::vector participants(cModule *radio) @@ -133,6 +147,9 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener notifications++; auto radio = check_and_cast(source); auto target = check_and_cast(value); + observedModeSet = target; + ASSERT(cachingObserver.modeSet == target); + ASSERT(cachingObserver.notifications == notifications); // Publication must follow *all* internal updates, irrespective of where // this observer appears in the subscriber list. auto nic = radio->getParentModule(); @@ -154,7 +171,12 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); radio->setMode(ht->getMode(Mbps(24))); - radio->subscribe(modesetChangedSignal, this); + cachingObserver.modeSet = ht; + observedModeSet = ht; + radio->subscribe(modesetChangedSignal, &cachingObserver); + // Hierarchical delivery guarantees the radio's ordinary caching + // listener runs before this ordinary throwing listener on its parent. + nic->subscribe(modesetChangedSignal, this); mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); check(nic, ht, true); auto oldGeneration = mib->findPeerHtState(peer)->generation; @@ -193,21 +215,22 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener ASSERT(arf->getRate(peer) == oldArf); ASSERT(onoe->getRate(peer) == oldOnoe); } - radio->unsubscribe(modesetChangedSignal, this); + nic->unsubscribe(modesetChangedSignal, this); + radio->unsubscribe(modesetChangedSignal, &cachingObserver); continue; } - // Reject after all internal updates have succeeded, to cover arbitrary - // notification-prefix failure in addition to fixed-rate lookup failure. + // Throw after an ordinary observer has cached the published set. + // Both observers and all behavioral participants must retain it. rejectNotification = true; bool rejected = false; try { radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); } catch (const cRuntimeError&) { rejected = true; } ASSERT(rejected); ASSERT(notifications == 1); - check(nic, ht, true); - ASSERT(mib->findPeerHtState(peer)->generation == oldGeneration); - ASSERT(channelAccess->getCw() == oldCw); - ASSERT(edcaf->getCw() == oldQosCw); + check(nic, legacy, false); + ASSERT(tx->getMode() == legacy->getMode(Mbps(24))); + ASSERT(cachingObserver.modeSet == tx->getModeSet()); + ASSERT(observedModeSet == tx->getModeSet()); rejectNotification = false; radio->setModeSet(legacy); check(nic, legacy, false); @@ -216,7 +239,10 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); check(nic, ht, true); ASSERT(notifications == 3); - radio->unsubscribe(modesetChangedSignal, this); + ASSERT(cachingObserver.modeSet == tx->getModeSet()); + ASSERT(observedModeSet == tx->getModeSet()); + nic->unsubscribe(modesetChangedSignal, this); + radio->unsubscribe(modesetChangedSignal, &cachingObserver); } std::cout << "Mode-set switching, advertisements, peer state, both selectors, and rollback verified.\n"; endSimulation(); From 1a3049475207fa8ce6365772cd566c06f7872d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 22:22:09 +0200 Subject: [PATCH 11/23] ieee80211: publish listening changes after observer failures A throwing mode-set observer prevented RadioMedium from receiving the listening change after radio state had already committed. Attempt both independent notifications before rethrowing the first observer failure, retaining committed state and the reentrancy guard during publication. Extend the module test to verify RadioMedium processes the listening change and cover listening-only and simultaneous observer failures through both mode-set setters. The regression fails before the fix and passes afterward. Debug build and scoped architecture checks pass. --- .../ieee80211/packetlevel/Ieee80211Radio.cc | 15 +++- .../module/Ieee80211ModeSetTransition_1.test | 83 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 81e4996c737..3dd0cb1ee7f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -8,6 +8,7 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" #include +#include #include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" @@ -154,16 +155,26 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 // The transaction is committed. Observer failures must not undo a state // already published to earlier listeners. Keep the reentrancy guard during // publication so every listener observes the same committed mode set. + std::exception_ptr observerFailure; try { if (modeSet != nullptr) emit(modesetChangedSignal, const_cast(modeSet)); + } + catch (...) { + observerFailure = std::current_exception(); + } + // Listening changes are independent committed facts: the medium must get + // its publication attempt even when a mode-set observer throws. + try { emit(listeningChangedSignal, 0); } catch (...) { - changingModeSet = false; - throw; + if (!observerFailure) + observerFailure = std::current_exception(); } changingModeSet = false; + if (observerFailure) + std::rethrow_exception(observerFailure); EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; } diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test index 50e9669bf32..fbb09387ff6 100644 --- a/tests/module/Ieee80211ModeSetTransition_1.test +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -2,11 +2,13 @@ Exercise both radio mode-set setters with DCF and HCF selectors. Successful HT/legacy transitions refresh PHY-limited capabilities and actual Beacon bodies. Fixed-rate failures restore all subscribed catalogs, caches, peer negotiations, -and channel-access windows. Observer failures leave published state committed. +and channel-access windows. Observer failures leave published state committed, +publish both notifications, and reach RadioMedium's listening-change handler. %file: Ieee80211ModeSetTransition.cc #include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/common/medium/RadioMedium.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" @@ -20,6 +22,24 @@ using namespace inet; using namespace inet::ieee80211; using namespace inet::physicallayer; +class TransitionRadioMedium : public RadioMedium +{ + public: + int listeningNotifications = 0; + cComponent *lastListeningSource = nullptr; + protected: + virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override + { + Enter_Method_Silent(); + RadioMedium::receiveSignal(source, signal, value, details); + if (signal == IRadio::listeningChangedSignal) { + listeningNotifications++; + lastListeningSource = source; + } + } +}; +Define_Module(TransitionRadioMedium); + class TransitionMgmtAp : public Ieee80211MgmtAp { bool probing = false; @@ -77,6 +97,8 @@ class CachingModeSetObserver : public cListener class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener { bool rejectNotification = false; + bool rejectListeningNotification = false; + int listeningNotifications = 0; int notifications = 0; const Ieee80211ModeSet *observedModeSet = nullptr; CachingModeSetObserver cachingObserver; @@ -141,6 +163,14 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener protected: virtual void initialize() override { scheduleAt(SimTime(1, SIMTIME_US), new cMessage("switch")); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override + { + Enter_Method_Silent(); + ASSERT(signal == IRadio::listeningChangedSignal); + listeningNotifications++; + if (rejectListeningNotification) + throw cRuntimeError("test observer rejects listening change"); + } virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { Enter_Method_Silent(); @@ -222,10 +252,14 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener // Throw after an ordinary observer has cached the published set. // Both observers and all behavioral participants must retain it. rejectNotification = true; + auto medium = check_and_cast(getParentModule()->getSubmodule("radioMedium")); + int oldListeningNotifications = medium->listeningNotifications; bool rejected = false; try { radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); } catch (const cRuntimeError&) { rejected = true; } ASSERT(rejected); + ASSERT(medium->listeningNotifications == oldListeningNotifications + 1); + ASSERT(medium->lastListeningSource == radio); ASSERT(notifications == 1); check(nic, legacy, false); ASSERT(tx->getMode() == legacy->getMode(Mbps(24))); @@ -241,6 +275,44 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener ASSERT(notifications == 3); ASSERT(cachingObserver.modeSet == tx->getModeSet()); ASSERT(observedModeSet == tx->getModeSet()); + // The medium subscribes on the radio, so it processes listening + // changes before the throwing observer on the parent interface. + radio->setMode(ht->getMode(Mbps(24))); + nic->subscribe(IRadio::listeningChangedSignal, this); + for (bool explicitMode : {false, true}) { + for (bool rejectModeSet : {false, true}) { + rejectNotification = rejectModeSet; + rejectListeningNotification = true; + int oldNotifications = notifications; + int oldListening = listeningNotifications; + int oldMediumListening = medium->listeningNotifications; + auto target = rejectModeSet ? ht : legacy; + bool caught = false; + try { + if (explicitMode) + radio->setModeSetAndMode(target, target->getMode(Mbps(24))); + else + radio->setModeSet(target); + } + catch (const cRuntimeError& error) { + caught = true; + const char *expected = rejectModeSet ? "test observer rejects mode set" : "test observer rejects listening change"; + ASSERT(std::string(error.what()).find(expected) != std::string::npos); + } + ASSERT(caught); + ASSERT(notifications == oldNotifications + 1); + ASSERT(listeningNotifications == oldListening + 1); + ASSERT(medium->listeningNotifications == oldMediumListening + 1); + ASSERT(medium->lastListeningSource == radio); + check(nic, target, false); + } + } + rejectNotification = false; + rejectListeningNotification = false; + // A subsequent transition also proves the reentrancy guard cleared. + radio->setModeSet(ht); + check(nic, ht, false); + nic->unsubscribe(IRadio::listeningChangedSignal, this); nic->unsubscribe(modesetChangedSignal, this); radio->unsubscribe(modesetChangedSignal, &cachingObserver); } @@ -262,6 +334,12 @@ simple TransitionMgmtAp extends Ieee80211MgmtAp @class(::TransitionMgmtAp); } +module TransitionRadioMedium extends Ieee80211ScalarRadioMedium +{ + parameters: + @class(::TransitionRadioMedium); +} + simple Ieee80211ModeSetTransitionTest extends SimpleModule { parameters: @@ -271,7 +349,7 @@ simple Ieee80211ModeSetTransitionTest extends SimpleModule network TransitionNetwork { submodules: - radioMedium: Ieee80211ScalarRadioMedium; + radioMedium: TransitionRadioMedium; ap[3]: AccessPoint; test: Ieee80211ModeSetTransitionTest; } @@ -285,6 +363,7 @@ seed-set = 0 cmdenv-express-mode = false record-vector-results = false record-scalar-results = false +*.radioMedium.listeningFilter = true **.mobility.initFromDisplayString = false **.mobility.initialX = 10m **.mobility.initialY = 10m From f07596adb9e2ad7dcfcc024b7a0664767557afea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 01:48:24 +0200 Subject: [PATCH 12/23] feature(physicallayer): add HT Greenfield mode set and PHY transmission support Register the n(greenfield-2.4Ghz) mode set and distinguish selectable operational modes (containsMode) from supported PHY capabilities (supportsMode). HT Greenfield profiles now explicitly support non-HT and HT-mixed response modes without making them selectable for data transmissions. Precompute immutable control-response mappings in Ieee80211ModeSet and make the mode-set registry thread-local to ensure thread safety. In the physical layer, validate per-packet transmission requests and reception feasibility against supported mode capabilities. Decompose transmission duration into preamble, header, and data intervals, correctly accounting for HT/VHT SIG field integration in the preamble. Publish modesetChangedSignal upon radio mode set transitions. Add unit test coverage for HT Greenfield compliant modes, duration decomposition, transmitter transition invariants, and ERP mode isolation. --- .../ieee80211/mode/Ieee80211HtMode.cc | 2 +- .../ieee80211/mode/Ieee80211ModeSet.cc | 439 +++++++++++++++++- .../ieee80211/mode/Ieee80211ModeSet.h | 36 +- .../ieee80211/mode/Ieee80211VhtMode.cc | 6 +- .../ieee80211/packetlevel/Ieee80211Radio.cc | 33 +- .../ieee80211/packetlevel/Ieee80211Radio.ned | 4 +- .../packetlevel/Ieee80211Receiver.cc | 4 +- .../packetlevel/Ieee80211Receiver.ned | 3 +- .../packetlevel/Ieee80211Transmitter.cc | 17 +- .../packetlevel/Ieee80211Transmitter.ned | 2 +- tests/unit/Ieee80211HtGreenfield_1.test | 354 ++++++++++++++ tests/unit/Ieee80211HtGuardInterval_1.test | 3 +- 12 files changed, 856 insertions(+), 47 deletions(-) create mode 100644 tests/unit/Ieee80211HtGreenfield_1.test diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index d4498568496..884fc3864f0 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -380,7 +380,7 @@ const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee802 const Ieee80211HtDataMode *dataMode = new Ieee80211HtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211HtPreambleMode *preambleMode = new Ieee80211HtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211HtMode *htMode = new Ieee80211HtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); + singleton.modeCache.emplace(htModeId, htMode); return htMode; } return mode->second; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index e21dbf9d613..e58712e453a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -9,6 +9,8 @@ #include #include +#include +#include #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" @@ -25,7 +27,147 @@ namespace physicallayer { Register_Abstract_Class(Ieee80211ModeSet); -const DelayedInitializer> Ieee80211ModeSet::modeSets([]() { return new std::vector { +std::vector Ieee80211ModeSet::completeHtGuardIntervalVariants(const char *name, const std::vector& entries) +{ + if (strcmp(name, "n(mixed-2.4Ghz)") && strcmp(name, "n(greenfield-2.4Ghz)")) + return entries; + + std::vector completeEntries = entries; + // IEEE Std 802.11-2024, Table 19-6 defines the 800 ns and 400 ns GIs. + // Add only the alternate GI for each mode explicitly declared above; + // declaration order, mandatory flags, and the historical catalog remain + // authoritative for this operation mode. + auto numberOfBaseEntries = completeEntries.size(); + for (size_t index = 0; index < numberOfBaseEntries; index++) { + auto htMode = dynamic_cast(completeEntries[index].mode); + if (htMode == nullptr) + continue; + auto dataMode = htMode->getDataMode(); + auto alternateGuardInterval = dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG ? + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT : Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG; + auto alternateMode = Ieee80211HtCompliantModes::getCompliantMode( + dataMode->getModulationAndCodingScheme(), htMode->getCenterFrequencyMode(), + htMode->getPreambleMode()->getPreambleFormat(), alternateGuardInterval); + auto alternateAlreadyPresent = std::any_of(completeEntries.begin(), completeEntries.end(), [alternateMode](const Entry& entry) { return entry.mode == alternateMode; }); + if (!alternateAlreadyPresent) + completeEntries.push_back({false, alternateMode}); + } + return completeEntries; +} + +#define HT_MODE_ENTRY(WIDTH, MCS, MANDATORY, FORMAT, GUARD_INTERVAL) \ + { MANDATORY, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs##MCS##BW##WIDTH##MHz, Ieee80211HtMode::BAND_2_4GHZ, FORMAT, GUARD_INTERVAL) }, +#define HT_MODE_ENTRIES_20(FORMAT) \ + HT_MODE_ENTRY(20, 0, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 1, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 2, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 3, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 4, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 5, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 6, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 7, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 8, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 9, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 10, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 11, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 12, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 13, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 14, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 15, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 16, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 17, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 18, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 19, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 20, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 21, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 22, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 23, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 24, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 25, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 26, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 27, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 28, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 29, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 30, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 31, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) +#define HT_MODE_ENTRIES_40(FORMAT) \ + HT_MODE_ENTRY(40, 0, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 1, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 2, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 3, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 4, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 5, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 6, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 7, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 8, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 9, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 10, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 11, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 12, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 13, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 14, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 15, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 16, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 17, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 18, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 19, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 20, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 21, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 22, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 23, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 24, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 25, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 26, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 27, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 28, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 29, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 30, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 31, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + +static std::vector createHtEntries(Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat) +{ + return { + HT_MODE_ENTRIES_20(preambleFormat) + HT_MODE_ENTRIES_40(preambleFormat) + }; +} + +static std::vector createHtSupportedEntries(Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat) +{ + auto result = createHtEntries(preambleFormat); + if (preambleFormat == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD) { + auto mixedEntries = createHtEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); + result.insert(result.end(), mixedEntries.begin(), mixedEntries.end()); + } + // Every 2.4 GHz HT STA supports the mandatory Clause 16/18 modes, and a + // Greenfield STA additionally supports HT-mixed PPDUs (IEEE 802.11-2024 + // 19.1.1 and 19.1.4). These are supplementary capabilities rather than + // selectable operating modes, so they are kept out of createHtEntries(). + result.insert(result.end(), { + { true, &Ieee80211DsssCompliantModes::dsssMode1Mbps, true }, + { true, &Ieee80211DsssCompliantModes::dsssMode2Mbps, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode9Mbps }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode18Mbps }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode36Mbps }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode48Mbps }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode54Mbps }, + }); + return result; +} + +#undef HT_MODE_ENTRIES_40 +#undef HT_MODE_ENTRIES_20 +#undef HT_MODE_ENTRY + +OPP_THREAD_LOCAL const DelayedInitializer> Ieee80211ModeSet::modeSets([]() { return new std::vector { Ieee80211ModeSet("a", { { true, &Ieee80211OfdmCompliantModes::ofdmMode6MbpsCS20MHz, true }, { false, &Ieee80211OfdmCompliantModes::ofdmMode9MbpsCS20MHz, true }, @@ -220,7 +362,73 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, true }, { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, true }, { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, true } - }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true), + }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true, createHtSupportedEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED)), + Ieee80211ModeSet("n(greenfield-2.4Ghz)", { + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs1BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs2BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs3BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs4BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs5BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs6BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs7BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs8BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs9BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs10BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs11BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs12BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs13BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs14BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs15BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs16BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs17BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs18BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs19BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs20BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs21BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs22BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs23BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs24BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs25BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs26BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs27BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs28BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs29BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs30BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs31BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs1BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs2BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs3BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs4BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs5BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs6BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs7BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs8BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs9BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs10BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs11BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs12BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs13BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs14BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs15BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs16BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs17BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs18BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs19BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs20BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs21BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs22BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs23BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs24BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs25BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs26BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs27BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs28BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs29BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs30BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs31BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) } + }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true, createHtSupportedEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD)), Ieee80211ModeSet("ac", { { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs1BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, @@ -542,12 +750,16 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe }, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::VHT),}; }); Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector entries, const IIeee80211Mode *referenceMode, - PhyType phyType, bool htOperationSupported) : + PhyType phyType, bool htOperationSupported, const std::vector supportedEntries) : name(name), - entries(entries), + entries(completeHtGuardIntervalVariants(name, entries)), phyType(phyType), referenceMode(referenceMode), - htOperationSupported(htOperationSupported) + htOperationSupported(htOperationSupported), + supportedEntries(supportedEntries.empty() ? this->entries : completeHtGuardIntervalVariants(name, supportedEntries)), + controlResponseModes(createControlResponseModes(this->supportedEntries)), + htMixedControlResponseModes(createHtMixedControlResponseModes(this->supportedEntries)), + nonHtControlResponseEntries(createNonHtControlResponseEntries(this->supportedEntries)) { if (this->entries.empty()) throw cRuntimeError("IEEE 802.11 mode set '%s' must contain at least one mode", this->name.c_str()); @@ -587,7 +799,7 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en // 9.4.2.3 and 11.1.4.6); management splits it across the primary and // Extended Supported Rates elements when necessary. for (bool mandatory : {true, false}) { - for (const auto& entry : this->entries) { + for (const auto& entry : this->supportedEntries) { if (entry.isLegacyOperational && entry.isMandatory == mandatory) this->legacyOperationalModes.push_back(entry.mode); } @@ -600,10 +812,14 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en for (const auto *mode : this->legacyOperationalModes) { if (mode == nullptr) throw cRuntimeError("IEEE 802.11 mode set '%s' contains a null legacy operational mode", this->name.c_str()); - int modeIndex = findModeIndex(mode); - if (modeIndex < 0) - throw cRuntimeError("Legacy operational mode '%s' is not contained in IEEE 802.11 mode set '%s'", mode->getName(), this->name.c_str()); - if (!this->entries[modeIndex].isLegacyOperational) + bool found = false; + for (const auto& entry : this->supportedEntries) { + if (entry.mode == mode && entry.isLegacyOperational) { + found = true; + break; + } + } + if (!found) throw cRuntimeError("Legacy operational mode '%s' is not marked eligible in IEEE 802.11 mode set '%s'", mode->getName(), this->name.c_str()); } for (const auto& entry : this->entries) { @@ -651,6 +867,109 @@ int Ieee80211ModeSet::findModeIndex(const IIeee80211Mode *mode) const return -1; } +std::map Ieee80211ModeSet::createControlResponseModes(const std::vector& supportedEntries) +{ + std::map result; + // IEEE 802.11-2024 Table 9-230 defines the Basic HT-MCS Set as a BSS- + // configured bitmap of MCS indexes. It is not modelled here, so use the + // mandatory entries (currently the 20 MHz entries) as a bounded fallback + // for the candidate indexes. Clause 10.6.6.5.3 selects CH_BANDWIDTH + // separately, and the candidate filter below then keeps only modes at the + // source bandwidth. Consequently, optional 40 MHz MCS 0..7 are treated as + // candidate MCSs. A modelled Basic HT-MCS Set would replace this mandatory- + // index fallback with the BSS-configured indexes; bandwidth filtering would + // remain a separate step. + std::set mandatoryHtMcsIndexes; + for (const auto& entry : supportedEntries) { + auto mode = dynamic_cast(entry.mode); + if (entry.isMandatory && mode != nullptr) + mandatoryHtMcsIndexes.insert(mode->getDataMode()->getMcsIndex()); + } + for (const auto& sourceEntry : supportedEntries) { + auto source = dynamic_cast(sourceEntry.mode); + if (source == nullptr) + continue; + std::vector candidates; + for (const auto& candidateEntry : supportedEntries) { + auto candidate = dynamic_cast(candidateEntry.mode); + if (candidate != nullptr && candidate->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + candidate->getCenterFrequencyMode() == source->getCenterFrequencyMode() && + candidate->getDataMode()->getBandwidth() == source->getDataMode()->getBandwidth() && + mandatoryHtMcsIndexes.find(candidate->getDataMode()->getMcsIndex()) != mandatoryHtMcsIndexes.end() && + candidate->getDataMode()->getMcsIndex() <= source->getDataMode()->getMcsIndex() && + candidate->getDataMode()->getNumberOfSpatialStreams() <= source->getDataMode()->getNumberOfSpatialStreams()) + candidates.push_back(candidate); + } + // IEEE 802.11-2024 10.6.6.5.3: with no Basic HT-MCS Set modelled, + // CandidateMCSSet uses the mandatory-index fallback. After the + // bandwidth and MCS-index bounds, + // retain the highest NSS not exceeding the received NSS, then select the + // highest indexed MCS whose per-stream modulation and coding rate do not + // exceed those of the received MCS. The modelled MCS 0..31 are EQM. + int highestNss = -1; + for (auto candidate : candidates) + highestNss = std::max(highestNss, candidate->getDataMode()->getNumberOfSpatialStreams()); + const Ieee80211HtMode *response = nullptr; + auto sourceMcs = source->getDataMode()->getModulationAndCodingScheme(); + int sourceModulation = sourceMcs->getModulation()->getSubcarrierModulation()->getCodeWordSize(); + double sourceCodeRate = sourceMcs->getCode()->getForwardErrorCorrection()->getCodeRate(); + for (auto candidate : candidates) { + auto candidateDataMode = candidate->getDataMode(); + auto candidateMcs = candidateDataMode->getModulationAndCodingScheme(); + if (candidateDataMode->getNumberOfSpatialStreams() == highestNss && + candidateMcs->getModulation()->getSubcarrierModulation()->getCodeWordSize() <= sourceModulation && + candidateMcs->getCode()->getForwardErrorCorrection()->getCodeRate() <= sourceCodeRate && + (response == nullptr || candidateDataMode->getMcsIndex() > response->getDataMode()->getMcsIndex())) + response = candidate; + } + if (response != nullptr) + result.emplace(sourceEntry.mode, response); + } + return result; +} + +std::map Ieee80211ModeSet::createHtMixedControlResponseModes(const std::vector& supportedEntries) +{ + std::map result; + for (const auto& sourceEntry : supportedEntries) { + auto source = dynamic_cast(sourceEntry.mode); + if (source == nullptr) + continue; + for (const auto& candidateEntry : supportedEntries) { + auto candidate = dynamic_cast(candidateEntry.mode); + if (candidate != nullptr && candidate->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + candidate->getCenterFrequencyMode() == source->getCenterFrequencyMode() && + candidate->getDataMode()->getMcsIndex() == source->getDataMode()->getMcsIndex() && + candidate->getDataMode()->getBandwidth() == source->getDataMode()->getBandwidth() && + candidate->getDataMode()->getNumberOfSpatialStreams() == source->getDataMode()->getNumberOfSpatialStreams() && + candidate->getDataMode()->getGuardIntervalType() == source->getDataMode()->getGuardIntervalType()) + { + result.emplace(sourceEntry.mode, candidateEntry.mode); + break; + } + } + } + return result; +} + +std::vector Ieee80211ModeSet::createNonHtControlResponseEntries(const std::vector& supportedEntries) +{ + std::vector result; + for (const auto& entry : supportedEntries) + if (entry.isMandatory && dynamic_cast(entry.mode) == nullptr && dynamic_cast(entry.mode) == nullptr) + result.push_back(entry); + std::stable_sort(result.begin(), result.end(), EntryNetBitrateComparator()); + return result; +} + +bool Ieee80211ModeSet::supportsMode(const IIeee80211Mode *mode) const +{ + for (const auto& entry : supportedEntries) + if (entry.mode == mode) + return true; + return false; +} + int Ieee80211ModeSet::getModeIndex(const IIeee80211Mode *mode) const { int index = findModeIndex(mode); @@ -662,7 +981,32 @@ int Ieee80211ModeSet::getModeIndex(const IIeee80211Mode *mode) const bool Ieee80211ModeSet::getIsMandatory(const IIeee80211Mode *mode) const { - return entries[getModeIndex(mode)].isMandatory; + int index = findModeIndex(mode); + if (index >= 0) + return entries[index].isMandatory; + for (const auto *legacyMode : legacyOperationalModes) { + if (legacyMode == mode) { + for (const auto& entry : supportedEntries) { + if (entry.mode == mode) + return entry.isMandatory; + } + } + } + throw cRuntimeError("Unknown mode"); +} + +const IIeee80211Mode *Ieee80211ModeSet::findMode(const IIeee80211Mode *mode) const +{ + int index = findModeIndex(mode); + return index >= 0 ? entries[index].mode : nullptr; +} + +const IIeee80211Mode *Ieee80211ModeSet::getMode(const IIeee80211Mode *mode) const +{ + auto result = findMode(mode); + if (result == nullptr) + throw cRuntimeError("Unknown mode in operation mode: '%s'", getName()); + return result; } const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode *mode) const @@ -843,6 +1187,78 @@ const IIeee80211Mode *Ieee80211ModeSet::getFasterMandatoryMode(const IIeee80211M return result; } +const IIeee80211Mode *Ieee80211ModeSet::getControlResponseMode(const IIeee80211Mode *mode, const IIeee80211Mode *configuredMode) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + auto it = controlResponseModes.find(mode); + if (it == controlResponseModes.end()) { + if (configuredMode != nullptr) + return getNonHtControlResponseMode(configuredMode, false); + return getMandatoryControlResponseMode(mode); + } + auto primaryMode = it->second; + if (configuredMode == nullptr) + return primaryMode; + // A configured HT response is a deliberate model extension beyond the + // automatic response constraints in IEEE 802.11-2024 10.6.6.5.3 and + // 10.6.6.5.7. Translate only its preamble to HT-mixed, preserving the + // resolved configured mode's MCS, bandwidth, NSS, GI, and band. + if (!supportsMode(configuredMode)) + throw cRuntimeError("Configured control response mode is not supported by operation mode %s: %s", getName(), configuredMode->getName()); + auto configuredIt = htMixedControlResponseModes.find(configuredMode); + if (configuredIt == htMixedControlResponseModes.end()) + throw cRuntimeError("An HT RTS requires an HT-mixed CTS response, configured mode is non-HT: %s", configuredMode->getName()); + return configuredIt->second; +} + +const IIeee80211Mode *Ieee80211ModeSet::getMandatoryControlResponseMode(const IIeee80211Mode *mode) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + if (!nonHtControlResponseEntries.empty() && (controlResponseModes.find(mode) != controlResponseModes.end() || !containsMode(mode))) + return getNonHtControlResponseMode(mode); + if (getIsMandatory(mode)) + return mode; + if (auto slowerMode = getSlowerMandatoryMode(mode)) + return slowerMode; + return getNonHtControlResponseMode(mode); +} + +const IIeee80211Mode *Ieee80211ModeSet::getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + if (nonHtControlResponseEntries.empty()) { + if (!containsMode(mode)) + throw cRuntimeError("No non-HT control response mode for %s", mode->getName()); + if (!mandatory) + return mode; + if (getIsMandatory(mode)) + return mode; + if (auto slowerMode = getSlowerMandatoryMode(mode)) + return slowerMode; + throw cRuntimeError("No mandatory control response mode for %s", mode->getName()); + } + if (controlResponseModes.find(mode) == controlResponseModes.end()) { + // VHT response-format selection remains unchanged; this fallback is HT-scoped. + if (dynamic_cast(mode) != nullptr) + return mode; + if (!mandatory) + return mode; + } + const IIeee80211Mode *result = nullptr; + for (const auto& entry : nonHtControlResponseEntries) { + auto candidate = entry.mode; + if (candidate->getDataMode()->getNetBitrate() <= mode->getDataMode()->getNetBitrate() && + (result == nullptr || candidate->getDataMode()->getNetBitrate() > result->getDataMode()->getNetBitrate())) + result = candidate; + } + if (result == nullptr) + throw cRuntimeError("No mandatory non-HT control response mode for %s", mode->getName()); + return result; +} + const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) { for (size_t index = 0; index < (&modeSets)->size(); index++) { @@ -871,4 +1287,3 @@ const Ieee80211ModeSet *Ieee80211ModeSet::getModeSet(const char *mode) } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index 0e1a3b6254f..5061b1a827e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -28,7 +28,6 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject VHT, }; - protected: class INET_API Entry { public: bool isMandatory; @@ -53,17 +52,26 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject std::set htSupportedChannelWidths; std::set htShortGuardIntervalChannelWidths; bool htOperationSupported = false; + // Entries are selectable modes; supportedEntries also contains immutable PHY capabilities needed for mandatory control responses. + const std::vector supportedEntries; + const std::map controlResponseModes; + const std::map htMixedControlResponseModes; + const std::vector nonHtControlResponseEntries; public: - static const DelayedInitializer> modeSets; + static OPP_THREAD_LOCAL const DelayedInitializer> modeSets; protected: + static std::vector completeHtGuardIntervalVariants(const char *name, const std::vector& entries); int findModeIndex(const IIeee80211Mode *mode) const; int getModeIndex(const IIeee80211Mode *mode) const; + static std::map createControlResponseModes(const std::vector& supportedEntries); + static std::map createHtMixedControlResponseModes(const std::vector& supportedEntries); + static std::vector createNonHtControlResponseEntries(const std::vector& supportedEntries); public: Ieee80211ModeSet(const char *name, const std::vector entries, const IIeee80211Mode *referenceMode, - PhyType phyType, bool htOperationSupported = false); + PhyType phyType, bool htOperationSupported = false, const std::vector supportedEntries = {}); virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "Ieee80211ModeSet, name = " << name; } @@ -86,12 +94,21 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const std::set& getHtShortGuardIntervalChannelWidths() const { return htShortGuardIntervalChannelWidths; } bool isHtShortGuardIntervalSupported(Hz bandwidth) const { return htShortGuardIntervalChannelWidths.count(bandwidth) != 0; } + // containsMode() covers entries selectable as the persistent operating mode + // (for example through Ieee80211Transmitter::setMode()). supportsMode() + // additionally covers immutable PHY capabilities that may be selected per + // packet through Ieee80211ModeReq, such as legacy control responses in a + // 2.4 GHz HT profile. bool containsMode(const IIeee80211Mode *mode) const { return findModeIndex(mode) != -1; } + bool supportsMode(const IIeee80211Mode *mode) const; bool getIsMandatory(const IIeee80211Mode *mode) const; // Finds a mode with the same PHY tuple as mode. Unlike findMode(), this // treats an absent guard interval (negative value) as an exact value. const IIeee80211Mode *findCompatibleMode(const IIeee80211Mode *mode) const; + // Pointer lookup is intentionally strict. Use getControlResponseMode() for an explicitly requested response that needs HT-mixed translation. + const IIeee80211Mode *findMode(const IIeee80211Mode *mode) const; + const IIeee80211Mode *getMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; @@ -106,6 +123,18 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const IIeee80211Mode *getSlowerMandatoryMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getFasterMandatoryMode(const IIeee80211Mode *mode) const; + // Automatic responses follow IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7. + // A configured CTS mode must be selectable and, for an HT RTS, must be HT; + // it is translated only to the corresponding HT-mixed response. An explicitly + // configured HT mode is a deliberate override and may bypass those response + // constraints. A legacy configured CTS rate is rejected by rate-selection + // initialization, while this API keeps the direct HT/legacy combination fatal. + const IIeee80211Mode *getControlResponseMode(const IIeee80211Mode *mode, const IIeee80211Mode *configuredMode = nullptr) const; + const IIeee80211Mode *getMandatoryControlResponseMode(const IIeee80211Mode *mode) const; + // 2.4 GHz HT modes are converted to non-HT responses. With mandatory=false, + // the input bitrate is used as a ceiling for selecting that non-HT mode. + const IIeee80211Mode *getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory = true) const; + static const Ieee80211ModeSet *findModeSet(const char *mode); static const Ieee80211ModeSet *getModeSet(const char *mode); @@ -129,4 +158,3 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index ea12fd18697..cd3b3eb82e4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -696,8 +696,8 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 throw cRuntimeError("Unsupported VHT preamble format: only HT_PREAMBLE_MIXED is supported (IEEE Std 802.11-2024, 21.3.2)"); const char *name = ""; // TODO unsigned int nss = mcsMode->getNumNss(); - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); - auto mode = singleton.modeCache.find(htModeId); + auto vhtModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); + auto mode = singleton.modeCache.find(vhtModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmSignalMode *legacySignal = nullptr; const Ieee80211VhtSignalMode *htSignal = nullptr; @@ -715,7 +715,7 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 const Ieee80211VhtDataMode *dataMode = new Ieee80211VhtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211VhtPreambleMode *preambleMode = new Ieee80211VhtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211VhtMode *htMode = new Ieee80211VhtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair(htModeId, htMode)); + singleton.modeCache.emplace(vhtModeId, htMode); return htMode; } return mode->second; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 3dd0cb1ee7f..eea8e96af86 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -156,21 +156,23 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 // already published to earlier listeners. Keep the reentrancy guard during // publication so every listener observes the same committed mode set. std::exception_ptr observerFailure; - try { - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); - } - catch (...) { - observerFailure = std::current_exception(); - } - // Listening changes are independent committed facts: the medium must get - // its publication attempt even when a mode-set observer throws. - try { - emit(listeningChangedSignal, 0); - } - catch (...) { - if (!observerFailure) + if (getComponentType() != nullptr) { + try { + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); + } + catch (...) { observerFailure = std::current_exception(); + } + // Listening changes are independent committed facts: the medium must get + // its publication attempt even when a mode-set observer throws. + try { + emit(listeningChangedSignal, 0); + } + catch (...) { + if (!observerFailure) + observerFailure = std::current_exception(); + } } changingModeSet = false; if (observerFailure) @@ -184,7 +186,8 @@ void Ieee80211Radio::setMode(const IIeee80211Mode *mode) ieee80211Transmitter->setMode(mode); EV << "Changing radio mode to " << mode << endl; receptionTimer = nullptr; - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setBand(const IIeee80211Band *band) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned index c32061114d3..0ac071271e4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned @@ -25,7 +25,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.FlatRadioBase; module Ieee80211Radio extends FlatRadioBase { parameters: - string opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-2.4Ghz)", "p", "ac") = default("g(mixed)"); // Operation mode + string opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)", "p", "ac") = default("g(mixed)"); // Operation mode string bandName @enum("2.4 GHz", "5 GHz", "5 GHz (20 MHz)", "5 GHz (40 MHz)", "5 GHz (80 MHz)", "5 GHz (160 MHz)", "5.9 GHz") = default("2.4 GHz"); // Band name int channelNumber = default(0); // Initial channel number within the band (TODO this is offset by 1) string fcsMode @enum("declared","computed") = default("declared"); @@ -47,6 +47,6 @@ module Ieee80211Radio extends FlatRadioBase *.channelNumber = this.channelNumber; @class(Ieee80211Radio); @signal[radioChannelChanged](type=long); + @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); @statistic[radioChannel](title="Radio channel"; source=radioChannelChanged; record=histogram,vector; interpolationmode=sample-hold); } - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 4c9d2ecd6d1..ae6804b4a84 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -50,13 +50,13 @@ std::ostream& Ieee80211Receiver::printToStream(std::ostream& stream, int level, bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, const ITransmission *transmission) const { auto ieee80211Transmission = dynamic_cast(transmission); - return ieee80211Transmission && modeSet->containsMode(ieee80211Transmission->getMode()) && NarrowbandReceiverBase::computeIsReceptionPossible(listening, transmission); + return ieee80211Transmission && modeSet->supportsMode(ieee80211Transmission->getMode()) && NarrowbandReceiverBase::computeIsReceptionPossible(listening, transmission); } bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, const IReception *reception, IRadioSignal::SignalPart part) const { auto ieee80211Transmission = dynamic_cast(reception->getTransmission()); - return ieee80211Transmission && modeSet->containsMode(ieee80211Transmission->getMode()) && getAnalogModel()->computeIsReceptionPossible(listening, reception, sensitivity); + return ieee80211Transmission && modeSet->supportsMode(ieee80211Transmission->getMode()) && getAnalogModel()->computeIsReceptionPossible(listening, reception, sensitivity); } const IReceptionResult *Ieee80211Receiver::computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned index c0c19e2afc2..29e9fc5cd80 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned @@ -23,7 +23,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandReceiverBas module Ieee80211Receiver extends NarrowbandReceiverBase { parameters: - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); int channelNumber; modulation = default("BPSK"); // TODO this is simply wrong @@ -32,4 +32,3 @@ module Ieee80211Receiver extends NarrowbandReceiverBase *.opMode = this.opMode; @class(Ieee80211Receiver); } - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 7011071f1d5..8ab34442799 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -49,7 +49,7 @@ const IIeee80211Mode *Ieee80211Transmitter::computeTransmissionMode(const Packet const auto& modeReq = const_cast(packet)->findTag(); const auto& bitrateReq = const_cast(packet)->findTag(); if (modeReq != nullptr) { - if (modeSet != nullptr && !modeSet->containsMode(modeReq->getMode())) + if (modeSet != nullptr && !modeSet->supportsMode(modeReq->getMode())) throw cRuntimeError("Unsupported mode requested"); transmissionMode = modeReq->getMode(); } @@ -166,9 +166,18 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); - const simtime_t headerDuration = transmissionMode->getHeaderDuration(); - const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); + const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); + const simtime_t modeledDataDuration = transmissionMode->getDataMode()->getDuration(B(phyHeader->getLengthField())); + // HT/VHT include their SIG fields in the PHY preamble duration, so their + // mode duration is exactly preamble + modeled data. Other PHYs expose a + // separate header; their residual data interval may also include a trailing + // signal extension (ERP). Keep that extension in chronological data time. + const bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); + const simtime_t dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; + if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || + preambleDuration + headerDuration + dataDuration != duration) + throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); auto analogModel = getAnalogModel()->createAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, transmissionBandwidth, transmissionPower); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, nullptr, nullptr, nullptr, nullptr, analogModel, transmissionMode, transmissionChannel); } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned index 3d50ab670e5..9c8b409b751 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned @@ -17,7 +17,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandTransmitter module Ieee80211Transmitter extends NarrowbandTransmitterBase { parameters: - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); int channelNumber; modulation = default("BPSK"); // TODO: This is simply wrong diff --git a/tests/unit/Ieee80211HtGreenfield_1.test b/tests/unit/Ieee80211HtGreenfield_1.test new file mode 100644 index 00000000000..11a5efa886f --- /dev/null +++ b/tests/unit/Ieee80211HtGreenfield_1.test @@ -0,0 +1,354 @@ +%description: +Checks that the 802.11n mixed and Greenfield mode profiles preserve distinct +preamble formats and timing, retain strict membership, and support the legacy +2.4 GHz HT capabilities used for non-HT control responses. Also checks VHT cache identity. + +%includes: +#include +#include + +#include "inet/common/packet/Packet.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TestIeee80211Transmitter : public Ieee80211Transmitter +{ + public: + const Ieee80211ModeSet *getTestModeSet() const { return modeSet; } + const IIeee80211Mode *getTestMode() const { return mode; } + const IIeee80211Mode *getTestTransmissionMode(const Packet *packet) const { return computeTransmissionMode(packet); } +}; + +static const Ieee80211HtMode *findMcs0LongGiMode(const Ieee80211ModeSet *modeSet) +{ + auto mode = dynamic_cast(modeSet->getMode(Mbps(6.5), MHz(20), 1)); + return mode != nullptr && mode->getDataMode()->getModulationAndCodingScheme()->getMcsIndex() == 0 && + mode->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG ? mode : nullptr; +} + +static bool rejectsUnknownMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + try { + modeSet->getMode(mode); + return false; + } + catch (const cRuntimeError&) { + return true; + } +} + +static bool rejectsMandatoryLookup(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + try { + modeSet->getIsMandatory(mode); + return false; + } + catch (const cRuntimeError&) { + return true; + } +} + +static bool rejectsLegacyConfiguredCts(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *htMode, const IIeee80211Mode *legacyMode) +{ + try { + modeSet->getControlResponseMode(htMode, legacyMode); + return false; + } + catch (const cRuntimeError& error) { + return std::string(error.getFormattedMessage()).find("HT RTS requires an HT-mixed CTS response") != std::string::npos; + } +} + +static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode) +{ + auto duration = mode->getDuration(B(0)); + auto preambleDuration = mode->getPreambleMode()->getDuration(); + auto modeledDataDuration = mode->getDataMode()->getDuration(B(0)); + bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + auto headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : mode->getHeaderMode()->getDuration(); + auto dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; + ASSERT(preambleDuration >= SIMTIME_ZERO); + ASSERT(headerDuration >= SIMTIME_ZERO); + ASSERT(dataDuration >= SIMTIME_ZERO); + ASSERT(preambleDuration + headerDuration + dataDuration == duration); + if (dynamic_cast(mode) != nullptr) + ASSERT(headerDuration == SIMTIME_ZERO); + if (dynamic_cast(mode) != nullptr) + ASSERT(dataDuration > modeledDataDuration); + if (dynamic_cast(mode) != nullptr && dynamic_cast(mode) == nullptr) + ASSERT(headerDuration > SIMTIME_ZERO); +} + +%activity: +auto mixedProfile = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +auto greenfieldProfile = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"); +ASSERT(mixedProfile != nullptr); +ASSERT(greenfieldProfile != nullptr); +ASSERT(mixedProfile == Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); +ASSERT(greenfieldProfile == Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)")); + +auto mixedMode = findMcs0LongGiMode(mixedProfile); +auto greenfieldMode = findMcs0LongGiMode(greenfieldProfile); +ASSERT(mixedMode != nullptr); +ASSERT(greenfieldMode != nullptr); +ASSERT(mixedMode != greenfieldMode); +ASSERT(mixedMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_2_4GHZ); +ASSERT(greenfieldMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_2_4GHZ); +ASSERT(mixedMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); +ASSERT(greenfieldMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(mixedMode->getPreambleMode()->getDuration() == SimTime(36, SIMTIME_US)); +ASSERT(greenfieldMode->getPreambleMode()->getDuration() == SimTime(24, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) == SimTime(40, SIMTIME_US)); +ASSERT(greenfieldMode->getDuration(B(0)) == SimTime(28, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) - greenfieldMode->getDuration(B(0)) == SimTime(12, SIMTIME_US)); +ASSERT(mixedMode->getDataMode()->getDuration(B(0)) == SimTime(4, SIMTIME_US)); +ASSERT(greenfieldMode->getDataMode()->getDuration(B(0)) == SimTime(4, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) == mixedMode->getPreambleMode()->getDuration() + mixedMode->getDataMode()->getDuration(B(0))); +ASSERT(greenfieldMode->getDuration(B(0)) == greenfieldMode->getPreambleMode()->getDuration() + greenfieldMode->getDataMode()->getDuration(B(0))); +ASSERT(!mixedProfile->containsMode(greenfieldMode)); +ASSERT(!greenfieldProfile->containsMode(mixedMode)); +ASSERT(mixedProfile->getSlowerMode(greenfieldMode) == nullptr); +ASSERT(greenfieldProfile->getFasterMode(mixedMode) == nullptr); +ASSERT(rejectsMandatoryLookup(mixedProfile, greenfieldMode)); +ASSERT(rejectsMandatoryLookup(greenfieldProfile, mixedMode)); +ASSERT(rejectsUnknownMode(mixedProfile, greenfieldMode)); +ASSERT(rejectsUnknownMode(greenfieldProfile, mixedMode)); +ASSERT(mixedProfile->supportsMode(mixedMode)); +ASSERT(greenfieldProfile->supportsMode(greenfieldMode)); +ASSERT(greenfieldProfile->supportsMode(mixedMode)); +ASSERT(!mixedProfile->supportsMode(greenfieldMode)); +ASSERT(mixedProfile->getSifsTime() == SimTime(10, SIMTIME_US)); +ASSERT(greenfieldProfile->getSifsTime() == SimTime(10, SIMTIME_US)); +ASSERT(mixedProfile->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(greenfieldProfile->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(mixedProfile->getCwMin() == 15); +ASSERT(greenfieldProfile->getCwMin() == 15); +const IIeee80211Mode *mandatoryLegacyModes[] = { + &Ieee80211DsssCompliantModes::dsssMode1Mbps, + &Ieee80211DsssCompliantModes::dsssMode2Mbps, + &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, +}; +for (auto legacyMode : mandatoryLegacyModes) { + ASSERT(mixedProfile->supportsMode(legacyMode)); + ASSERT(greenfieldProfile->supportsMode(legacyMode)); +} +const IIeee80211Mode *supplementaryOnlyLegacyModes[] = { + &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble, +}; +for (auto legacyMode : supplementaryOnlyLegacyModes) { + ASSERT(!mixedProfile->containsMode(legacyMode)); + ASSERT(!greenfieldProfile->containsMode(legacyMode)); +} +ASSERT(Ieee80211DsssCompliantModes::dsssMode1Mbps.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211DsssCompliantModes::dsssMode2Mbps.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(mixedProfile->getNonHtControlResponseMode(mixedMode) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(greenfieldProfile->getNonHtControlResponseMode(greenfieldMode) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(greenfieldProfile->getNonHtControlResponseMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode9Mbps) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(mixedProfile->getControlResponseMode(mixedMode) == mixedMode); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMode) == mixedMode); + +auto greenfieldMcs8 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldProfile->supportsMode(greenfieldMcs8)); +// MCS 8 is optional and uses two BPSK 1/2 streams. With no Basic HT-MCS Set +// modelled, 10.6.6.5.3 selects mandatory one-stream MCS 0 for the CTS. +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8) == mixedMode); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8, greenfieldMode) == mixedMode); +ASSERT(rejectsLegacyConfiguredCts(greenfieldProfile, greenfieldMode, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps)); + +auto greenfieldMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +auto mixedMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +auto greenfieldMcs15 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs7) == mixedMcs7); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs15) == mixedMcs7); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs7, greenfieldMode) == mixedMode); + +auto mixedMcs15 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +auto configuredMixedResponse = dynamic_cast(greenfieldProfile->getControlResponseMode(greenfieldMcs7, greenfieldMcs15)); +ASSERT(configuredMixedResponse == mixedMcs15); +ASSERT(configuredMixedResponse->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); +ASSERT(configuredMixedResponse->getCenterFrequencyMode() == greenfieldMcs15->getCenterFrequencyMode()); +ASSERT(configuredMixedResponse->getDataMode()->getMcsIndex() == greenfieldMcs15->getDataMode()->getMcsIndex()); +ASSERT(configuredMixedResponse->getDataMode()->getBandwidth() == greenfieldMcs15->getDataMode()->getBandwidth()); +ASSERT(configuredMixedResponse->getDataMode()->getNumberOfSpatialStreams() == greenfieldMcs15->getDataMode()->getNumberOfSpatialStreams()); +ASSERT(configuredMixedResponse->getDataMode()->getGuardIntervalType() == greenfieldMcs15->getDataMode()->getGuardIntervalType()); +ASSERT(mixedProfile->getMandatoryControlResponseMode(mixedMcs7) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); +ASSERT(mixedProfile->getMandatoryControlResponseMode(mixedMcs15) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); +ASSERT(mixedProfile->getNonHtControlResponseMode(mixedMcs15, false) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); + +auto greenfieldMcs8Bw40 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW40MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +auto mixedMcs0Bw40 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs0BW40MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +// The bounded no-Basic-HT-MCS-Set approximation reuses mandatory 20 MHz MCS +// indexes for candidates at the source bandwidth, so optional 40 MHz MCS 8 +// maps to 40 MHz MCS 0 (without changing the established response behavior). +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8Bw40) == mixedMcs0Bw40); + +auto transmitter = new TestIeee80211Transmitter; +transmitter->setModeSet(greenfieldProfile); +transmitter->setMode(greenfieldMode); +transmitter->setModeSet(mixedProfile); +ASSERT(transmitter->getTestModeSet() == mixedProfile); +ASSERT(transmitter->getTestMode() == mixedMode); +transmitter->setModeSet(greenfieldProfile); +ASSERT(transmitter->getTestModeSet() == greenfieldProfile); +ASSERT(transmitter->getTestMode() == greenfieldMode); +Packet supportedLegacyPacket("supportedLegacy"); +supportedLegacyPacket.addTag()->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(transmitter->getTestTransmissionMode(&supportedLegacyPacket) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setModeSet(mixedProfile); +ASSERT(transmitter->getTestTransmissionMode(&supportedLegacyPacket) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(transmitter->getTestMode() == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setMode(mixedMode); +transmitter->setModeSet(greenfieldProfile); +try { + transmitter->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestMode() == greenfieldMode); +} +try { + transmitter->setMode(mixedMode); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestMode() == greenfieldMode); +} +transmitter->setModeSet(nullptr); +ASSERT(transmitter->getTestModeSet() == nullptr); +ASSERT(transmitter->getTestMode() == nullptr); +delete transmitter; + +auto ofdmProfile = Ieee80211ModeSet::getModeSet("a"); +auto halfRateProfile = Ieee80211ModeSet::getModeSet("p"); +auto ofdm6 = ofdmProfile->getMode(Mbps(6), MHz(20), 1); +transmitter = new TestIeee80211Transmitter; +transmitter->setModeSet(ofdmProfile); +transmitter->setMode(ofdm6); +try { + transmitter->setModeSet(halfRateProfile); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestModeSet() == ofdmProfile); + ASSERT(transmitter->getTestMode() == ofdm6); +} +delete transmitter; + +auto greenfield5GhzMode = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs0BW20MHz, + Ieee80211HtMode::BAND_5GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(greenfield5GhzMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_5GHZ); +ASSERT(greenfield5GhzMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(!greenfieldProfile->containsMode(greenfield5GhzMode)); +ASSERT(!greenfieldProfile->supportsMode(greenfield5GhzMode)); + +auto vhtMixed5GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +bool rejectedVhtGreenfield = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (const cRuntimeError&) { + rejectedVhtGreenfield = true; +} +ASSERT(rejectedVhtGreenfield); +auto vhtMixed2GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_2_4GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(vhtMixed5GhzMode != vhtMixed2GhzMode); +ASSERT(vhtMixed5GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_5GHZ); +ASSERT(vhtMixed2GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_2_4GHZ); +ASSERT(vhtMixed5GhzMode->getDuration(B(0)) == vhtMixed5GhzMode->getPreambleMode()->getDuration() + vhtMixed5GhzMode->getDataMode()->getDuration(B(0))); +ASSERT(vhtMixed5GhzMode->getDataMode()->getDuration(B(0)) >= SIMTIME_ZERO); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6))); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode); + +auto erpMixedProfile = Ieee80211ModeSet::getModeSet("g(mixed)"); +auto erpOnlyProfile = Ieee80211ModeSet::getModeSet("g(erp)"); +auto erpMixedMode = &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps; +auto erpOnlyMode = &Ieee80211ErpOfdmCompliantModes::erpOnlyOfdmMode6Mbps; +Ieee80211DsssOfdmMode dsssOfdmMode("dsss-ofdm", nullptr, nullptr, nullptr, nullptr, &Ieee80211OfdmCompliantModes::ofdmDataMode6MbpsCS20MHz); +ASSERT(erpMixedProfile != nullptr); +ASSERT(erpOnlyProfile != nullptr); +assertTransmitterDurationDecomposition(erpMixedProfile->getMode(Mbps(6))); +ASSERT(erpMixedMode != erpOnlyMode); +ASSERT(erpMixedMode->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(erpOnlyMode->getSlotTime() == SimTime(9, SIMTIME_US)); +ASSERT(!erpMixedProfile->containsMode(erpOnlyMode)); +ASSERT(!erpOnlyProfile->containsMode(erpMixedMode)); +ASSERT(erpMixedProfile->findMode(erpOnlyMode) == nullptr); +ASSERT(erpOnlyProfile->findMode(erpMixedMode) == nullptr); +ASSERT(erpMixedProfile->findMode(&dsssOfdmMode) == nullptr); +ASSERT(rejectsUnknownMode(erpMixedProfile, erpOnlyMode)); +ASSERT(rejectsUnknownMode(erpOnlyProfile, erpMixedMode)); + +EV << "HT mixed and Greenfield modes have distinct cached identities and timing.\n"; + +%contains: stdout +HT mixed and Greenfield modes have distinct cached identities and timing. diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 11e854a6a0c..05f3ca83153 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -37,10 +37,11 @@ class TestIeee80211Receiver : public Ieee80211Receiver class TestIeee80211Radio : public Ieee80211Radio { public: - void setup(ITransmitter *tx, IReceiver *rx) + void setup(Ieee80211Transmitter *tx, Ieee80211Receiver *rx) { transmitter = tx; receiver = rx; + setComponentType(cModuleType::get("inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio")); } }; From 22dfa410818c6c1360c74204f905164f2f8b6b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 01:48:34 +0200 Subject: [PATCH 13/23] feature(linklayer): implement HT control-response rate selection and Greenfield integration Apply IEEE 802.11-2024 control-response rules in DCF and QoS rate selection: ordinary HT ACK and Basic BlockAck responses use mandatory non-HT rates, and CTS responses to HT-carried RTS frames use the HT-mixed format. Translate configured CTS response rates to their corresponding HT-mixed counterparts while preserving MCS, bandwidth, NSS, and guard interval. Enforce mode set initialization invariants by failing fast if RateSelection has no mode set at link-layer initialization, and rebuild configured modes atomically on dynamic mode-set changes via modesetChangedSignal. Add "n(greenfield-2.4Ghz)" to wireless interface and MAC module NEDs, and provide an end-to-end Greenfield ping simulation example in omnetpp-ht-greenfield.ini. Document backward compatibility notes in WHATSNEW regarding 802.11n control-response rate selection. Add comprehensive unit and module test coverage for rate selection, dynamic mode-set rebinding, and HT Greenfield and Mixed runtime exchanges. --- WHATSNEW | 7 + .../lan80211/omnetpp-ht-greenfield.ini | 37 ++++ .../ieee80211/ExtUpperIeee80211Interface.ned | 3 +- .../ieee80211/Ieee80211Interface.ned | 3 +- .../linklayer/ieee80211/mac/Ieee80211Mac.ned | 3 +- .../mac/rateselection/QosRateSelection.cc | 103 +++++---- .../mac/rateselection/QosRateSelection.h | 2 - .../mac/rateselection/QosRateSelection.ned | 10 + .../mac/rateselection/RateSelection.cc | 74 +++++-- .../mac/rateselection/RateSelection.h | 2 - .../mac/rateselection/RateSelection.ned | 7 + ...e80211ConfiguredResponseRateSelection.test | 206 ++++++++++++++++++ .../module/Ieee80211HtGreenfieldRuntime.test | 132 +++++++++++ tests/module/Ieee80211HtMixedRuntime.test | 131 +++++++++++ .../Ieee80211InvalidConfiguredCtsRate.test | 134 ++++++++++++ .../Ieee80211LegacyResponseRateSelection.test | 110 ++++++++++ ...ateSelectionNullModeSetInitialization.test | 54 +++++ ...ateSelectionNullModeSetInitialization.test | 54 +++++ tests/unit/Ieee80211HtGuardInterval_1.test | 41 ++-- 19 files changed, 1027 insertions(+), 86 deletions(-) create mode 100644 examples/wireless/lan80211/omnetpp-ht-greenfield.ini create mode 100644 tests/module/Ieee80211ConfiguredResponseRateSelection.test create mode 100644 tests/module/Ieee80211HtGreenfieldRuntime.test create mode 100644 tests/module/Ieee80211HtMixedRuntime.test create mode 100644 tests/module/Ieee80211InvalidConfiguredCtsRate.test create mode 100644 tests/module/Ieee80211LegacyResponseRateSelection.test create mode 100644 tests/module/QosRateSelectionNullModeSetInitialization.test create mode 100644 tests/module/RateSelectionNullModeSetInitialization.test diff --git a/WHATSNEW b/WHATSNEW index 5fdd7668423..e98e81ea3c0 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -186,6 +186,13 @@ Notable backward incompatible changes are the following: These changes are backward incompatible for C++ code that directly references the old combined ICMP error indication or tag types. +7. 802.11n control-response rate selection + + Ordinary ACK and Basic BlockAck responses in the n(mixed-2.4Ghz) profile now + use the mandatory non-HT fallback because BSSBasicRateSet is not modelled. + This can change airtime, timeout trajectories, throughput, and fingerprints + in existing simulations. An HT RTS continues to receive an HT-mixed CTS. + Notable backward compatible changes are the following: 1. IPv6 network configurator diff --git a/examples/wireless/lan80211/omnetpp-ht-greenfield.ini b/examples/wireless/lan80211/omnetpp-ht-greenfield.ini new file mode 100644 index 00000000000..9d59b48fcc6 --- /dev/null +++ b/examples/wireless/lan80211/omnetpp-ht-greenfield.ini @@ -0,0 +1,37 @@ +[General] +network = Lan80211 +abstract = true +cmdenv-express-mode = true + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 600m +**.constraintAreaMaxY = 400m +**.constraintAreaMaxZ = 0m + +**.arp.typename = "GlobalArp" +**.opMode = "n(greenfield-2.4Ghz)" +**.wlan[*].bitrate = 13Mbps + +*.ap.wlan[*].address = "10:00:00:00:00:00" +*.host[*].**.mgmt.accessPointAddress = "10:00:00:00:00:00" + +*.ap.mobility.typename = "StationaryMobility" +*.ap.mobility.initialX = 250m +*.ap.mobility.initialY = 200m +*.host[*].mobility.typename = "StationaryMobility" +*.host[*].mobility.initialY = 200m +*.host[0].mobility.initialX = 260m +*.host[1].mobility.initialX = 270m + +[Config HtGreenfield] +description = "HT Greenfield mode exchanging ping traffic through an access point" +sim-time-limit = 2s +*.numHosts = 2 +*.host[0].numApps = 0 +*.host[1].numApps = 1 +*.host[1].app[0].typename = "PingApp" +*.host[1].app[0].destAddr = "host[0]" +*.host[1].app[0].sendInterval = 100ms +*.host[1].app[0].printPing = true diff --git a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned index 83b958c0f9f..57161c6354f 100644 --- a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned +++ b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned @@ -27,7 +27,7 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface { parameters: string energySourceModule = default(""); - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); double bitrate @unit(bps) = default(-1bps); **.opMode = this.opMode; **.bitrate = this.bitrate; @@ -92,4 +92,3 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface classifier.in <-- { @display("m=n"); } <-- tap.lowerLayerOut; } - diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned index e32eaba59fc..3534c8d7c18 100644 --- a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned @@ -63,7 +63,7 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface parameters: string interfaceTableModule; string energySourceModule = default(""); - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); string address @mutable = default("auto"); // MAC address as hex string (12 hex digits), or // "auto". "auto" values will be replaced by // a generated MAC address in init stage 0. @@ -129,4 +129,3 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface classifier.in <-- { @display("m=n"); } <-- upperLayerIn; } - diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned index 51f4198660a..b3ffdfb777e 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned @@ -78,7 +78,7 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac { parameters: string mibModule; - string modeSet @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string modeSet @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); string fcsMode @enum("declared","computed") = default("declared"); string initialRadioMode @enum("off","sleep","receiver","transmitter","transceiver") = default("receiver"); @@ -135,4 +135,3 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac @display("p=250,200"); } } - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 7aa9b3aaec3..b885503275d 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -20,6 +20,23 @@ namespace ieee80211 { using namespace inet::physicallayer; +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +{ + if (bitrate == -1) + return nullptr; + try { + auto result = modeSet->getMode(bps(bitrate)); + if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) + throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); + return result; + } + catch (const cRuntimeError& error) { + throw cRuntimeError("%s has invalid responseCtsFrameBitrate=%g bps for operation mode '%s'; " + "the configured CTS rate must resolve to a selectable mode (HT RTS responses require an HT mode): %s", + modulePath, bitrate, modeSet->getName(), error.getFormattedMessage().c_str()); + } +} + Define_Module(QosRateSelection); void QosRateSelection::initialize(int stage) @@ -29,6 +46,8 @@ void QosRateSelection::initialize(int stage) mib.reference(this, "mibModule", true); if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); + if (modeSet == nullptr) + throw cRuntimeError("QosRateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); updateModes(); } } @@ -59,20 +78,29 @@ void QosRateSelection::updateModes() if (modeSet == nullptr) return; double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - responseBlockAckFrameMode = (responseBlockAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); + + multicastFrameMode = newMulticastFrameMode; + dataFrameMode = newDataFrameMode; + mgmtFrameMode = newMgmtFrameMode; + controlFrameMode = newControlFrameMode; + responseAckFrameMode = newResponseAckFrameMode; + responseBlockAckFrameMode = newResponseBlockAckFrameMode; + responseCtsFrameMode = newResponseCtsFrameMode; + fastestMandatoryMode = newFastestMandatoryMode; lastTransmittedFrameMode.clear(); perReceiverDataFrameMode.clear(); perReceiverResolved = false; @@ -109,36 +137,27 @@ bool QosRateSelection::isControlResponseFrame(const Ptr& dataOrMgmtHeader) { - // TODO BSSBasicRateSet, alternate rate - auto mode = getMode(packet, dataOrMgmtHeader); - ASSERT(modeSet->containsMode(mode)); - const IIeee80211Mode *responseMode; - if (!responseAckFrameMode) { - if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) - responseMode = mandatoryMode; - else - throw cRuntimeError("Mandatory mode not found"); + const IIeee80211Mode *responseMode = nullptr; + if (responseAckFrameMode) + responseMode = modeSet->getNonHtControlResponseMode(responseAckFrameMode, false); + else { + auto mode = getMode(packet, dataOrMgmtHeader); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1/10.6.6.5.2: this bounded model uses a + // mandatory non-HT rate for ordinary ACK responses; BSSBasicRateSet is not modelled. + responseMode = modeSet->getMandatoryControlResponseMode(mode); } - else - responseMode = responseAckFrameMode; - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); + return dataOrMgmtHeader ? getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame) { - // TODO BSSBasicRateSet, alternate rate auto mode = getMode(packet, rtsFrame); - ASSERT(modeSet->containsMode(mode)); - const IIeee80211Mode *responseMode; - if (!responseCtsFrameMode) { - if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) - responseMode = mandatoryMode; - else - throw cRuntimeError("Mandatory mode not found"); - } - else - responseMode = responseCtsFrameMode; - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); + ASSERT(modeSet->supportsMode(mode)); + // The eliciting mode is required even when a CTS rate is configured because + // the response format and CandidateMCSSet depend on the received PPDU. + auto responseMode = modeSet->getControlResponseMode(mode, responseCtsFrameMode); + return rtsFrame ? getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode) : responseMode; } // @@ -149,12 +168,19 @@ const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *pack // const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq) { - if (dynamicPtrCast(blockAckReq)) { - auto mode = responseBlockAckFrameMode ? responseBlockAckFrameMode : getMode(packet, blockAckReq); - return getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), mode); - } - else + if (!dynamicPtrCast(blockAckReq)) throw cRuntimeError("Unknown BlockAckReq frame type"); + const IIeee80211Mode *responseMode = nullptr; + if (responseBlockAckFrameMode) + responseMode = modeSet->getNonHtControlResponseMode(responseBlockAckFrameMode, false); + else { + auto mode = getMode(packet, blockAckReq); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.5.2 permits non-HT Basic BlockAck responses; + // this model has no BSSBasicRateSet/HT Control context to select another PPDU. + responseMode = modeSet->getNonHtControlResponseMode(mode, false); + } + return blockAckReq ? getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) @@ -288,11 +314,10 @@ const IIeee80211Mode *QosRateSelection::getPeerCompatibleMode(const MacAddress& // supported for HT (802.11n) modes using negotiated PeerHtState. Non-HT // modes (legacy and VHT) return unchanged because VHT capability negotiation // (VHT Capabilities/Operation elements) is not yet modeled in MIB. - if (mode == nullptr || peerAddress.isMulticast() || !mib || mode->getHtMcsIndex() < 0) + if (mode == nullptr || peerAddress.isMulticast() || !mib || mib->mode == Ieee80211Mib::INDEPENDENT || mode->getHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 53cb75b94e1..9914fe825b5 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -63,7 +63,6 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void updateModes(); - // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. @@ -96,4 +95,3 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 88ec4706b60..e3fb6369c95 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -27,8 +27,18 @@ simple QosRateSelection extends SimpleModule double multicastFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT BlockAck PPDU. double responseBlockAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. A configured value must resolve to a + // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed + // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a + // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index e2ef635a78e..6094d101bd6 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -24,6 +24,23 @@ namespace ieee80211 { using namespace inet::physicallayer; +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +{ + if (bitrate == -1) + return nullptr; + try { + auto result = modeSet->getMode(bps(bitrate)); + if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) + throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); + return result; + } + catch (const cRuntimeError& error) { + throw cRuntimeError("%s has invalid responseCtsFrameBitrate=%g bps for operation mode '%s'; " + "the configured CTS rate must resolve to a selectable mode (HT RTS responses require an HT mode): %s", + modulePath, bitrate, modeSet->getName(), error.getFormattedMessage().c_str()); + } +} + Define_Module(RateSelection); void RateSelection::initialize(int stage) @@ -34,6 +51,8 @@ void RateSelection::initialize(int stage) } else if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); + if (modeSet == nullptr) + throw cRuntimeError("RateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); updateModes(); // WATCH(dataOrMgmtRateControl); @@ -79,18 +98,28 @@ void RateSelection::updateModes() if (modeSet == nullptr) return; double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); + + // Commit only after every configured mode has been resolved, so a failed + // runtime operation-mode change leaves the previous state intact. + multicastFrameMode = newMulticastFrameMode; + dataFrameMode = newDataFrameMode; + mgmtFrameMode = newMgmtFrameMode; + controlFrameMode = newControlFrameMode; + responseAckFrameMode = newResponseAckFrameMode; + responseCtsFrameMode = newResponseCtsFrameMode; + fastestMandatoryMode = newFastestMandatoryMode; lastTransmittedFrameMode.clear(); perReceiverDataFrameMode.clear(); perReceiverResolved = false; @@ -115,26 +144,30 @@ const IIeee80211Mode *RateSelection::getMode(Packet *packet, const Ptr& dataOrMgmtHeader) { + // Keep configured responses independent of the eliciting packet; callers may + // resolve a configured response while constructing a test packet. + const IIeee80211Mode *responseMode = nullptr; if (responseAckFrameMode) - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseAckFrameMode); + responseMode = modeSet->getNonHtControlResponseMode(responseAckFrameMode, false); else { auto mode = getMode(packet, dataOrMgmtHeader); - ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1/10.6.6.5.2: this bounded model uses a + // mandatory non-HT rate for ordinary ACK responses; BSSBasicRateSet is not modelled. + responseMode = modeSet->getMandatoryControlResponseMode(mode); } + return dataOrMgmtHeader ? getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame) { - if (responseCtsFrameMode) - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseCtsFrameMode); - else { - auto mode = getMode(packet, rtsFrame); - ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); - } + auto mode = getMode(packet, rtsFrame); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1 and 10.6.6.5.7 require an HT response to an + // HT RTS and forbid HT-GF for the response. Consequently, even a configured + // CTS rate needs the eliciting PPDU's mode tag to select a legal format/MCS. + auto responseMode = modeSet->getControlResponseMode(mode, responseCtsFrameMode); + return rtsFrame ? getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode) : responseMode; } // 802.11-1999 Std. @@ -255,11 +288,10 @@ const IIeee80211Mode *RateSelection::getPeerCompatibleMode(const MacAddress& pee // supported for HT (802.11n) modes using negotiated PeerHtState. Non-HT // modes (legacy and VHT) return unchanged because VHT capability negotiation // (VHT Capabilities/Operation elements) is not yet modeled in MIB. - if (mode == nullptr || peerAddress.isMulticast() || !mib || mode->getHtMcsIndex() < 0) + if (mode == nullptr || peerAddress.isMulticast() || !mib || mib->mode == Ieee80211Mib::INDEPENDENT || mode->getHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 32294eae93c..9a3ba460e44 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -66,7 +66,6 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void updateModes(); - // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. @@ -107,4 +106,3 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 82938a1df50..60ae6786a07 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -23,7 +23,14 @@ simple RateSelection extends SimpleModule like IRateSelection double multicastFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. A configured value must resolve to a + // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed + // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a + // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest diff --git a/tests/module/Ieee80211ConfiguredResponseRateSelection.test b/tests/module/Ieee80211ConfiguredResponseRateSelection.test new file mode 100644 index 00000000000..e8212250c11 --- /dev/null +++ b/tests/module/Ieee80211ConfiguredResponseRateSelection.test @@ -0,0 +1,206 @@ +%description: +Checks through the public rate-selection API that 2.4 GHz HT ACK/BlockAck responses use +non-HT rates and configured HT CTS rates use exact HT-mixed counterparts. + +%file: Test.cc +#include +#include + +#include "inet/common/InitStages.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211ConfiguredResponseRateSelection { + +static void assertHtMixedResponse(const char *name, const IIeee80211Mode *mode) +{ + auto htMode = dynamic_cast(mode); + if (htMode == nullptr || htMode->getPreambleMode()->getPreambleFormat() != Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED || htMode->getDataMode()->getNetBitrate() != Mbps(6.5)) + throw cRuntimeError("%s did not return HT-mixed 6.5 Mbps (actual mode: %s, bitrate: %g)", name, mode->getName(), mode->getDataMode()->getNetBitrate().get()); +} + +static void assertNonHtResponse(const char *name, const IIeee80211Mode *mode) +{ + if (dynamic_cast(mode) != nullptr || mode->getDataMode()->getNetBitrate() != Mbps(6)) + throw cRuntimeError("%s did not return non-HT 6 Mbps", name); +} + +class ConfiguredResponseProbe : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + + auto dcfRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0].mac.dcf.rateSelection")); + auto qosRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0].mac.hcf.rateSelection")); + auto dynamicQosRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0].mac.hcf.rateSelection")); + auto dynamicWlan = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0]")); + auto dynamicMac = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0].mac")); + if (strcmp(dynamicWlan->par("opMode"), "n(mixed-2.4Ghz)") != 0 || strcmp(dynamicMac->par("modeSet"), "n(mixed-2.4Ghz)") != 0) + throw cRuntimeError("Dynamic response probe is not using the mixed profile (wlan: %s, mac: %s)", dynamicWlan->par("opMode").stringValue(), dynamicMac->par("modeSet").stringValue()); + + if (double(dcfRateSelection->par("responseAckFrameBitrate")) != 6.5e6 + || double(qosRateSelection->par("responseAckFrameBitrate")) != 6.5e6) + throw cRuntimeError("Configured response bitrate was not applied to both rate selections"); + + auto sourceMode = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)")->getMode(Mbps(6.5)); + auto mixedSourceMode = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")->getMode(Mbps(6.5)); + Packet packet("receivedFrame"); + packet.addTag()->setMode(sourceMode); + Packet mixedPacket("receivedMixedFrame"); + mixedPacket.addTag()->setMode(mixedSourceMode); + Packet nonHtPacket("receivedNonHtFrame"); + nonHtPacket.addTag()->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); + auto dataHeader = makeShared(); + dataHeader->setReceiverAddress(MacAddress("10:00:00:00:00:00")); + Packet dataPacket("configuredDataFrame"); + + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == sourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == sourceMode); + + assertNonHtResponse("DCF ACK", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + Packet untaggedPacket("untaggedRts"); + try { + dcfRateSelection->computeResponseCtsFrameMode(&untaggedPacket, nullptr); + throw cRuntimeError("Configured DCF CTS unexpectedly accepted an untagged eliciting packet"); + } + catch (const cRuntimeError& error) { + if (error.getFormattedMessage().find("Missing mode") == std::string::npos) + throw; + } + try { + qosRateSelection->computeResponseCtsFrameMode(&untaggedPacket, nullptr); + throw cRuntimeError("Configured QoS CTS unexpectedly accepted an untagged eliciting packet"); + } + catch (const cRuntimeError& error) { + if (error.getFormattedMessage().find("Missing mode") == std::string::npos) + throw; + } + assertHtMixedResponse("DCF CTS HT", dcfRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("DCF CTS non-HT", dcfRateSelection->computeResponseCtsFrameMode(&nonHtPacket, nullptr)); + assertNonHtResponse("QoS ACK", qosRateSelection->computeResponseAckFrameMode(&packet, nullptr)); + assertHtMixedResponse("QoS CTS HT", qosRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("QoS CTS non-HT", qosRateSelection->computeResponseCtsFrameMode(&nonHtPacket, nullptr)); + + auto basicBlockAckReq = makeShared(); + assertNonHtResponse("QoS Basic BlockAck", qosRateSelection->computeResponseBlockAckFrameMode(&packet, basicBlockAckReq)); + assertNonHtResponse("Dynamic mixed QoS ACK", dynamicQosRateSelection->computeResponseAckFrameMode(&mixedPacket, nullptr)); + assertHtMixedResponse("Dynamic mixed QoS CTS", dynamicQosRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertNonHtResponse("Dynamic mixed QoS Basic BlockAck", dynamicQosRateSelection->computeResponseBlockAckFrameMode(&mixedPacket, basicBlockAckReq)); + auto optionalHtMode = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); + Packet optionalHtPacket("receivedOptionalHtFrame"); + optionalHtPacket.addTag()->setMode(optionalHtMode); + assertHtMixedResponse("Configured CTS optional HT MCS", qosRateSelection->computeResponseCtsFrameMode(&optionalHtPacket, nullptr)); + auto greenfieldMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); + Packet greenfieldMcs7Packet("receivedGreenfieldMcs7Frame"); + greenfieldMcs7Packet.addTag()->setMode(greenfieldMcs7); + assertHtMixedResponse("Configured DCF CTS MCS 0 override for MCS 7", dcfRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); + assertHtMixedResponse("Configured CTS MCS 0 override for MCS 7", qosRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); + + // Exercise the actual signal path with a compatible mode-set transition. + // The configured data cache must bind to the exact profile object while + // the configured response operations remain usable after each refresh. + auto wlan = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0]")); + wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"))); + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == mixedSourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == mixedSourceMode); + assertHtMixedResponse("Rebound DCF CTS mixed", dcfRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertHtMixedResponse("Rebound QoS CTS mixed", qosRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertNonHtResponse("Rebound DCF ACK mixed", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS ACK mixed", qosRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS BlockAck mixed", qosRateSelection->computeResponseBlockAckFrameMode(nullptr, basicBlockAckReq)); + wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"))); + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == sourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == sourceMode); + assertHtMixedResponse("Rebound DCF CTS Greenfield", dcfRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertHtMixedResponse("Rebound QoS CTS Greenfield", qosRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("Rebound DCF ACK Greenfield", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS ACK Greenfield", qosRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS BlockAck Greenfield", qosRateSelection->computeResponseBlockAckFrameMode(nullptr, basicBlockAckReq)); + + std::cout << "Mixed and Greenfield ACK/Basic BlockAck use non-HT 6 Mbps; configured HT CTS uses the exact HT-mixed MCS override and requires an eliciting mode tag.\n"; + } +}; + +Define_Module(ConfiguredResponseProbe); + +} // namespace Ieee80211ConfiguredResponseRateSelection + +%file: test.ned + +import inet.examples.wireless.lan80211.Lan80211; + +simple ConfiguredResponseProbe +{ + @class(ConfiguredResponseProbe); +} + +network TestConfiguredResponses extends Lan80211 +{ + parameters: + numHosts = default(2); + submodules: + configuredResponseProbe: ConfiguredResponseProbe; +} + +%inifile: omnetpp.ini + +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestConfiguredResponses +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +[Config ConfiguredResponses] +abstract = false +network = TestConfiguredResponses +sim-time-limit = 1s +*.host[1].wlan[*].opMode = "n(mixed-2.4Ghz)" +**.opMode = "n(greenfield-2.4Ghz)" +**.qosStation = true +*.host[1].wlan[0].mac.hcf.rateSelection.responseAckFrameBitrate = -1bps +*.host[1].wlan[0].mac.hcf.rateSelection.responseCtsFrameBitrate = -1bps +*.host[1].wlan[0].mac.hcf.rateSelection.responseBlockAckFrameBitrate = -1bps +**.wlan[*].mac.dcf.rateSelection.responseAckFrameBitrate = 6.5Mbps +**.wlan[*].mac.dcf.rateSelection.responseCtsFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseAckFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseCtsFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseBlockAckFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameBandwidth = 20MHz +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameNumSpatialStreams = 1 +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 + +%extraargs: -c ConfiguredResponses + +%contains: stdout +Mixed and Greenfield ACK/Basic BlockAck use non-HT 6 Mbps; configured HT CTS uses the exact HT-mixed MCS override and requires an eliciting mode tag. diff --git a/tests/module/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test new file mode 100644 index 00000000000..60d013e74f3 --- /dev/null +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -0,0 +1,132 @@ +%description: + +Checks forced HT-Greenfield RTS/HT-mixed CTS plus unicast HT-Greenfield data and non-HT ACK responses between two ad hoc stations. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtGreenfieldRuntime { + +static OPP_THREAD_LOCAL int htGreenfieldRtsCount = 0; +static OPP_THREAD_LOCAL int htMixedCtsCount = 0; +static OPP_THREAD_LOCAL int htGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int nonHtAckCount = 0; + +class HtPreambleCheckingRadio : public Ieee80211Radio, public cListener +{ + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + auto isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; + auto isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + if (dynamicPtrCast(macHeader) && isHtGreenfield) + htGreenfieldRtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedCtsCount++; + if (dynamicPtrCast(macHeader) && isHtGreenfield) + htGreenfieldDataCount++; + if (dynamicPtrCast(macHeader) && mode == nullptr) + nonHtAckCount++; + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (htGreenfieldRtsCount == 0 || htMixedCtsCount == 0 || htGreenfieldDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected HT-GF RTS, HT-MF CTS, HT-GF data, and non-HT ACK transmissions"); + std::cout << "Observed " << htGreenfieldRtsCount << " HT-Greenfield RTS, " << htMixedCtsCount << " HT-mixed CTS, " << htGreenfieldDataCount << " HT-Greenfield data, and " << nonHtAckCount << " non-HT ACK response(s).\n"; + } +}; + +Define_Module(HtPreambleCheckingRadio); + +} // namespace Ieee80211HtGreenfieldRuntime + +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; + +module HtPreambleCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtPreambleCheckingRadio); +} + +network TestHtGreenfield +{ + parameters: + int numHosts = default(2); + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[numHosts]: AdhocHost; +} + +%inifile: omnetpp.ini + +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestHtGreenfield +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +**.wlan[*].radio.typename = "HtPreambleCheckingRadio" + +[Config HtPreambleRuntime] +abstract = false +network = TestHtGreenfield +sim-time-limit = 2s +*.numHosts = 2 +*.host[0].numApps = 1 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[1].numApps = 1 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 +*.host[0].app[0].sendInterval = 100ms +**.wlan[*].opMode = "n(greenfield-2.4Ghz)" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B + +%extraargs: -c HtPreambleRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* HT-Greenfield RTS, [1-9][0-9]* HT-mixed CTS, [1-9][0-9]* HT-Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test new file mode 100644 index 00000000000..cd81007886e --- /dev/null +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -0,0 +1,131 @@ +%description: +Checks a deterministic n(mixed-2.4Ghz) exchange with HT-mixed RTS/CTS and data +followed by an ERP-OFDM ACK. Basic BlockAck response selection is covered by +the focused public QoS API test because enabling a bounded BAR exchange would +require additional association and agreement setup. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtMixedRuntime { + +static OPP_THREAD_LOCAL int htMixedRtsCount = 0; +static OPP_THREAD_LOCAL int htMixedCtsCount = 0; +static OPP_THREAD_LOCAL int htMixedDataCount = 0; +static OPP_THREAD_LOCAL int erpOfdmAckCount = 0; + +class HtMixedCheckingRadio : public Ieee80211Radio, public cListener +{ + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *obj, cObject *) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedRtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedCtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedDataCount++; + if (dynamicPtrCast(macHeader) && dynamic_cast(transmission->getMode()) != nullptr) { + if (transmission->getMode()->getDataMode()->getBandwidth() != MHz(20)) + throw cRuntimeError("ERP-OFDM ACK did not use the 20 MHz channel bandwidth"); + erpOfdmAckCount++; + } + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (htMixedRtsCount == 0 || htMixedCtsCount == 0 || htMixedDataCount == 0 || erpOfdmAckCount == 0) + throw cRuntimeError("Expected HT-mixed RTS/CTS and data with ERP-OFDM ACK response"); + std::cout << "Observed " << htMixedRtsCount << " HT-mixed RTS, " << htMixedCtsCount << " HT-mixed CTS, " << htMixedDataCount << " HT-mixed data, and " << erpOfdmAckCount << " ERP-OFDM ACK response(s).\n"; + } +}; + +Define_Module(HtMixedCheckingRadio); + +} // namespace Ieee80211HtMixedRuntime + +%file: test.ned +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module HtMixedCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtMixedCheckingRadio); +} + +network TestHtMixedRuntime +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[2]: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestHtMixedRuntime +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 +**.wlan[*].radio.typename = "HtMixedCheckingRadio" + +[Config HtMixedRuntime] +abstract = false +network = TestHtMixedRuntime +sim-time-limit = 2s +*.host[0].numApps = 1 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[0].app[0].sendInterval = 100ms +*.host[1].numApps = 1 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B + +%extraargs: -c HtMixedRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* HT-mixed RTS, [1-9][0-9]* HT-mixed CTS, [1-9][0-9]* HT-mixed data, and [1-9][0-9]* ERP-OFDM ACK response\(s\). diff --git a/tests/module/Ieee80211InvalidConfiguredCtsRate.test b/tests/module/Ieee80211InvalidConfiguredCtsRate.test new file mode 100644 index 00000000000..294f3939aa0 --- /dev/null +++ b/tests/module/Ieee80211InvalidConfiguredCtsRate.test @@ -0,0 +1,134 @@ +%description: +Checks that a non-selectable legacy responseCtsFrameBitrate is rejected during +rate-selection resolution for both DCF and QoS selections and both 2.4 GHz HT profiles. + +%file: Test.cc +#include +#include +#include + +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211InvalidConfiguredCtsRate { + +template +static void checkRejected(const char *selectorName, Selector& selector) +{ + double bitrate = selector.par("responseCtsFrameBitrate"); + char formattedBitrate[64]; + std::snprintf(formattedBitrate, sizeof(formattedBitrate), "%g", bitrate); + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)"}) { + bool rejected = false; + try { + selector.resolveConfiguredModesForTest(Ieee80211ModeSet::getModeSet(profile)); + } + catch (const cRuntimeError& error) { + rejected = true; + auto message = error.getFormattedMessage(); + if (message.find(std::string("responseCtsFrameBitrate=") + formattedBitrate) == std::string::npos || + message.find("operation mode '" + std::string(profile) + "'") == std::string::npos || + message.find("selectable mode") == std::string::npos) + throw cRuntimeError("%s diagnostic for %s at %g bps is incomplete: %s", selectorName, profile, bitrate, message.c_str()); + } + if (!rejected) + throw cRuntimeError("%s accepted legacy CTS bitrate %g bps in %s", selectorName, bitrate, profile); + } +} + +class TestRateSelection : public RateSelection +{ + public: + void resolveConfiguredModesForTest(const Ieee80211ModeSet *modeSet) { applyModeSet(modeSet); } + + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) { + checkRejected("DCF RateSelection", *this); + std::cout << "DCF rejected configured legacy responseCtsFrameBitrate for both HT profiles.\n"; + } + } +}; + +class TestQosRateSelection : public QosRateSelection +{ + public: + void resolveConfiguredModesForTest(const Ieee80211ModeSet *modeSet) { applyModeSet(modeSet); } + + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) { + checkRejected("QoS RateSelection", *this); + std::cout << "QoS rejected configured legacy responseCtsFrameBitrate for both HT profiles.\n"; + } + } +}; + +Define_Module(TestRateSelection); +Define_Module(TestQosRateSelection); + +} // namespace Ieee80211InvalidConfiguredCtsRate + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; + +simple TestRateSelection extends RateSelection +{ + parameters: + @class(TestRateSelection); + rateControlModule = ""; + mibModule = ""; + responseCtsFrameBitrate = default(6Mbps); +} + +simple TestQosRateSelection extends QosRateSelection +{ + parameters: + @class(TestQosRateSelection); + rateControlModule = ""; + mibModule = ""; + responseCtsFrameBitrate = default(6Mbps); +} + +simple TestRateSelection24 extends TestRateSelection +{ + parameters: + responseCtsFrameBitrate = default(24Mbps); +} + +simple TestQosRateSelection24 extends TestQosRateSelection +{ + parameters: + responseCtsFrameBitrate = default(24Mbps); +} + +network TestInvalidConfiguredCtsRate +{ + submodules: + dcfRateSelection6: TestRateSelection; + dcfRateSelection24: TestRateSelection24; + qosRateSelection6: TestQosRateSelection; + qosRateSelection24: TestQosRateSelection24; +} + +%inifile: omnetpp.ini +[General] +network = TestInvalidConfiguredCtsRate +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 + +%contains: stdout +DCF rejected configured legacy responseCtsFrameBitrate for both HT profiles. +%contains: stdout +QoS rejected configured legacy responseCtsFrameBitrate for both HT profiles. diff --git a/tests/module/Ieee80211LegacyResponseRateSelection.test b/tests/module/Ieee80211LegacyResponseRateSelection.test new file mode 100644 index 00000000000..5fc607b867b --- /dev/null +++ b/tests/module/Ieee80211LegacyResponseRateSelection.test @@ -0,0 +1,110 @@ +%description: +Checks the public DCF and QoS response-rate APIs for representative legacy +operation modes, preserving mandatory ACK/CTS and Basic BlockAck selections. + +%file: Test.cc +#include + +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211LegacyResponseRateSelection { + +class TestRateSelection : public RateSelection +{ + public: + void setModeSetForTest(const Ieee80211ModeSet *newModeSet) { modeSet = newModeSet; } +}; + +class TestQosRateSelection : public QosRateSelection +{ + public: + void setModeSetForTest(const Ieee80211ModeSet *newModeSet) { modeSet = const_cast(newModeSet); } +}; + +struct LegacyProfileCase { + const char *name; + bps sourceBitrate; + bps expectedResponseBitrate; + bps expectedBlockAckBitrate; +}; + +static void checkCase(const LegacyProfileCase& testCase) +{ + auto modeSet = Ieee80211ModeSet::getModeSet(testCase.name); + auto sourceMode = modeSet->getMode(testCase.sourceBitrate); + auto expectedMode = modeSet->getMode(testCase.expectedResponseBitrate); + auto expectedBlockAckMode = modeSet->getMode(testCase.expectedBlockAckBitrate); + Packet packet("elicitingFrame"); + packet.addTag()->setMode(sourceMode); + auto basicBlockAckReq = makeShared(); + + TestRateSelection dcfRateSelection; + dcfRateSelection.setModeSetForTest(modeSet); + ASSERT(dcfRateSelection.computeResponseAckFrameMode(&packet, nullptr) == expectedMode); + ASSERT(dcfRateSelection.computeResponseCtsFrameMode(&packet, nullptr) == expectedMode); + + TestQosRateSelection qosRateSelection; + qosRateSelection.setModeSetForTest(modeSet); + ASSERT(qosRateSelection.computeResponseAckFrameMode(&packet, nullptr) == expectedMode); + ASSERT(qosRateSelection.computeResponseCtsFrameMode(&packet, nullptr) == expectedMode); + auto blockAckMode = qosRateSelection.computeResponseBlockAckFrameMode(&packet, basicBlockAckReq); + if (blockAckMode != expectedBlockAckMode) + throw cRuntimeError("%s Basic BlockAck returned %s, expected %s", testCase.name, blockAckMode->getName(), expectedBlockAckMode->getName()); +} + +class LegacyResponseProbe : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + const LegacyProfileCase cases[] = { + {"a", Mbps(54), Mbps(24), Mbps(54)}, + {"b", Mbps(11), Mbps(11), Mbps(11)}, + {"g(mixed)", Mbps(54), Mbps(24), Mbps(54)}, + }; + for (const auto& testCase : cases) + checkCase(testCase); + std::cout << "Legacy a, b, and g(mixed) DCF/QoS ACK/CTS and Basic BlockAck responses preserved.\n"; + } +}; + +Define_Module(LegacyResponseProbe); + +} // namespace Ieee80211LegacyResponseRateSelection + +%file: test.ned +simple LegacyResponseProbe +{ + parameters: + @class(LegacyResponseProbe); +} + +network TestLegacyResponseRateSelection +{ + submodules: + probe: LegacyResponseProbe; +} + +%inifile: omnetpp.ini +[General] +network = TestLegacyResponseRateSelection +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 + +%contains: stdout +Legacy a, b, and g(mixed) DCF/QoS ACK/CTS and Basic BlockAck responses preserved. diff --git a/tests/module/QosRateSelectionNullModeSetInitialization.test b/tests/module/QosRateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..976921dadc1 --- /dev/null +++ b/tests/module/QosRateSelectionNullModeSetInitialization.test @@ -0,0 +1,54 @@ +%description: +QosRateSelection must reject reaching link-layer initialization without a mode set. + +%file: Test.cc +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" + +using namespace inet; +using namespace inet::ieee80211; + +namespace QosRateSelectionNullModeSetInitialization { + +class NullModeSetQosRateSelection : public QosRateSelection +{ + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) + QosRateSelection::initialize(stage); + } +}; + +Define_Module(NullModeSetQosRateSelection); + +} // namespace QosRateSelectionNullModeSetInitialization + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; + +simple NullModeSetQosRateSelection extends QosRateSelection +{ + parameters: + @class(NullModeSetQosRateSelection); + rateControlModule = ""; + mibModule = ""; +} + +network TestQosRateSelectionNullModeSetInitialization +{ + submodules: + rateSelection: NullModeSetQosRateSelection; +} + +%inifile: omnetpp.ini +[General] +network = TestQosRateSelectionNullModeSetInitialization +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false + +%exitcode: 1 + +%contains-regex: stderr +QosRateSelection module TestQosRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization diff --git a/tests/module/RateSelectionNullModeSetInitialization.test b/tests/module/RateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..8358beb9972 --- /dev/null +++ b/tests/module/RateSelectionNullModeSetInitialization.test @@ -0,0 +1,54 @@ +%description: +RateSelection must reject reaching link-layer initialization without a mode set. + +%file: Test.cc +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" + +using namespace inet; +using namespace inet::ieee80211; + +namespace RateSelectionNullModeSetInitialization { + +class NullModeSetRateSelection : public RateSelection +{ + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) + RateSelection::initialize(stage); + } +}; + +Define_Module(NullModeSetRateSelection); + +} // namespace RateSelectionNullModeSetInitialization + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; + +simple NullModeSetRateSelection extends RateSelection +{ + parameters: + @class(NullModeSetRateSelection); + rateControlModule = ""; + mibModule = ""; +} + +network TestRateSelectionNullModeSetInitialization +{ + submodules: + rateSelection: NullModeSetRateSelection; +} + +%inifile: omnetpp.ini +[General] +network = TestRateSelectionNullModeSetInitialization +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false + +%exitcode: 1 + +%contains-regex: stderr +RateSelection module TestRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 05f3ca83153..aa9d38b49b7 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -37,7 +37,7 @@ class TestIeee80211Receiver : public Ieee80211Receiver class TestIeee80211Radio : public Ieee80211Radio { public: - void setup(Ieee80211Transmitter *tx, Ieee80211Receiver *rx) + void setup(ITransmitter *tx, IReceiver *rx) { transmitter = tx; receiver = rx; @@ -74,6 +74,10 @@ class TestRateSelectionAccessor : public ieee80211::RateSelection static void notifyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } + static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } }; class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection @@ -86,6 +90,10 @@ class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection static void notifyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } + static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } }; class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase @@ -470,24 +478,24 @@ rateSelection->par("multicastFrameBitrate").setDoubleValue(6e6); rateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); rateSelection->par("controlFrameBitrate").setDoubleValue(6e6); rateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); -rateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelection->par("responseCtsFrameBitrate").setDoubleValue(-1); rateSelection->finalizeParameters(); rateSelection->buildInside(); -TestRateSelectionAccessor::notifyModeSet(rateSelection, erpModeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == erpModeSet); +TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); -ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection) == nullptr); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); ASSERT(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection) == erpModeSet->getMode(Mbps(24))); // Dynamically switch rateSelection to HT mode set -TestRateSelectionAccessor::notifyModeSet(rateSelection, modeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == modeSet); +TestRateSelectionAccessor::applyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); @@ -495,7 +503,7 @@ ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMod ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); -ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection) == nullptr); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); // Also verify HT-specific fixed mode configured with bandwidth, stream, and guard interval @@ -510,14 +518,15 @@ rateSelectionHt->par("multicastFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("mgmtFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("controlFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("responseAckFrameBitrate").setDoubleValue(6e6); -rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6.5e6); rateSelectionHt->finalizeParameters(); rateSelectionHt->buildInside(); -TestRateSelectionAccessor::notifyModeSet(rateSelectionHt, modeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelectionHt) == modeSet); +TestRateSelectionAccessor::applyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelectionHt) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt))); ASSERT(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt) == short65); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelectionHt))); auto *qosRateSelection = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.QosRateSelection")->create("qosRateSelection", this)); qosRateSelection->par("rateControlModule").setStringValue(""); @@ -528,18 +537,18 @@ qosRateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("controlFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("responseBlockAckFrameBitrate").setDoubleValue(6e6); -qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(-1); qosRateSelection->finalizeParameters(); qosRateSelection->buildInside(); -TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, erpModeSet); -ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == erpModeSet); +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); -TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, modeSet); -ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == modeSet); +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); From e2c0eb1dfae645b6468c813d39b8e87e969e22dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 02:22:16 +0200 Subject: [PATCH 14/23] fix(ieee80211): negotiate HT Greenfield capability and fall back to HT-mixed for mixed peers - Mode & physical layer introspection: - Add virtual isHtGreenfield() query to IIeee80211Mode and override it in Ieee80211HtMode to identify Greenfield preamble modes. - Track htGreenfieldSupported in Ieee80211ModeSet constructor and expose isHtGreenfieldSupported() accessor. - Expose findHtMixedMode() on Ieee80211ModeSet to resolve the HT-mixed equivalent of any HT mode based on precomputed response tables. - MIB & directional capability negotiation: - Populate localHtCapabilities.greenfield from modeSet->isHtGreenfieldSupported() in Ieee80211Mib::updateLocalHtCapabilities. - Add receiverGreenfield to Ieee80211HtDirectionalCapabilities and populate directional flags during negotiateHtCapabilities(). - Rate selection peer filtering & HT-mixed fallback: - In isCompatibleHtMode(), reject candidate Greenfield modes when the negotiated receiver did not advertise Greenfield support. - In selectPeerCompatibleMode(), dynamically map Greenfield candidate modes to their legal HT-mixed equivalents, allowing Greenfield stations to communicate with mixed-format peers at high throughput before falling back to legacy rates. - Testing: - Update tests/unit/Ieee80211HtCapabilities_1.test with directional Greenfield capability assertions. - Add tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test verifying bidirectional unicast data delivery and dynamic HT-mixed frame format selection in a heterogeneous Greenfield/Mixed BSS. --- .../Ieee80211PeerModeSelection.cc | 17 +- .../ieee80211/mib/Ieee80211HtCapabilities.h | 5 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 1 + .../wireless/ieee80211/mode/IIeee80211Mode.h | 4 +- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 1 + .../ieee80211/mode/Ieee80211ModeSet.cc | 10 + .../ieee80211/mode/Ieee80211ModeSet.h | 4 + ...80211HtHeterogeneousGreenfieldRuntime.test | 199 ++++++++++++++++++ tests/unit/Ieee80211HtCapabilities_1.test | 3 + 9 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 048699e5980..93fc50b453a 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -84,6 +84,11 @@ static bool isCompatibleHtMode(const IIeee80211Mode *mode, const Ieee80211Mib::P bandwidth > negotiated.operation.operatingChannelWidth) return false; + // IEEE Std 802.11-2024, 19.1.1 and 19.1.4: an HT-greenfield PPDU may only + // be transmitted to a receiver that advertised HT-greenfield support. + if (mode->isHtGreenfield() && !receiverCapabilities.receiverGreenfield) + return false; + // IEEE Std 802.11-2024, 10.17 and Table 9-224: a short guard interval // is usable only when the receiver advertised it for this channel width. if (mode->isHtShortGuardInterval()) { @@ -133,14 +138,20 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, if (peerHtState == nullptr || !peerHtState->valid || !peerHtState->negotiatedCapabilities.localTxPeerRx.valid) return getLegacyFallback(modeSet, mode, peerAddress); - if (isCompatibleHtMode(mode, peerHtState)) - return mode; + + const IIeee80211Mode *effectiveMode = mode; + if (effectiveMode->isHtGreenfield() && !peerHtState->negotiatedCapabilities.localTxPeerRx.receiverGreenfield) + effectiveMode = modeSet->findHtMixedMode(mode); + if (effectiveMode != nullptr && isCompatibleHtMode(effectiveMode, peerHtState)) + return effectiveMode; auto candidateBitrate = mode->getDataMode()->getNetBitrate(); const IIeee80211Mode *bestMode = nullptr; for (int i = 0; i < modeSet->getNumModes(); i++) { const auto *candidate = modeSet->getMode(i); - if (candidate->getHtMcsIndex() < 0 || + if (candidate->isHtGreenfield() && !peerHtState->negotiatedCapabilities.localTxPeerRx.receiverGreenfield) + candidate = modeSet->findHtMixedMode(candidate); + if (candidate == nullptr || candidate->getHtMcsIndex() < 0 || candidate->getDataMode()->getNetBitrate() > candidateBitrate || !isCompatibleHtMode(candidate, peerHtState)) continue; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index 2174ce5c387..3b62b3399d4 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -72,6 +72,7 @@ struct Ieee80211HtDirectionalCapabilities Ieee80211HtMcsNssMap mcsNss; std::array supportedMcs = {}; bool receiverLdpc = false; + bool receiverGreenfield = false; bool receiverShortGi20 = false; bool receiverShortGi40 = false; int receiverMaxAmpduLengthExponent = 0; @@ -116,9 +117,11 @@ inline Ieee80211NegotiatedHtCapabilities negotiateHtCapabilities(const Ieee80211 negotiated.localRxPeerTx.mcsNss.maxMcsPerNss[nss] = mcs; } } - // LDPC and short-GI bits advertise receiver capability, so they are directional. + // LDPC, Greenfield, and short-GI bits advertise receiver capability, so they are directional. negotiated.localTxPeerRx.receiverLdpc = peer.ldpc; negotiated.localRxPeerTx.receiverLdpc = local.ldpc; + negotiated.localTxPeerRx.receiverGreenfield = peer.greenfield; + negotiated.localRxPeerTx.receiverGreenfield = local.greenfield; negotiated.localTxPeerRx.receiverShortGi20 = peer.shortGi20; negotiated.localRxPeerTx.receiverShortGi20 = local.shortGi20; negotiated.localTxPeerRx.receiverShortGi40 = peer.shortGi40; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 0d28b31a9c8..4ec66ed169c 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -172,6 +172,7 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS localHtCapabilities.txMcsNss.maxMcsPerNss[nss] = std::max(localHtCapabilities.txMcsNss.maxMcsPerNss[nss], mcs % 8); } } + localHtCapabilities.greenfield = modeSet->isHtGreenfieldSupported(); if (localHtCapabilities.supportedChannelWidths.empty()) throw cRuntimeError("HT operation mode set '%s' does not provide an HT channel width", modeSet->getName()); localHtCapabilities.maxAmpduLengthExponent = par("htMaxAmpduLengthExponent"); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index 8fa1b77bbc4..4a104cb221d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -63,6 +63,9 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject // Returns whether this mode uses the optional 400 ns HT guard interval. // Non-HT modes deliberately report false. virtual bool isHtShortGuardInterval() const = 0; + // Returns whether this mode uses the HT Greenfield preamble format. + // Non-HT modes and HT-mixed modes report false. + virtual bool isHtGreenfield() const { return false; } virtual int getLegacyCwMin() const = 0; virtual int getLegacyCwMax() const = 0; virtual const char *getName() const = 0; @@ -92,4 +95,3 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 5e5d43b6fa3..b35858ad69d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -248,6 +248,7 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual const Ieee80211HtDataMode *getDataMode() const override { return dataMode; } virtual int getHtMcsIndex() const override { return dataMode->getMcsIndex(); } virtual bool isHtShortGuardInterval() const override { return dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT; } + virtual bool isHtGreenfield() const override { return preambleMode->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; } virtual const Ieee80211HtPreambleMode *getPreambleMode() const override { return preambleMode; } virtual const Ieee80211HtSignalMode *getHeaderMode() const override { return preambleMode->getSignalMode(); } virtual const Ieee80211OfdmSignalMode *getLegacySignalMode() const { return preambleMode->getLegacySignalMode(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index e58712e453a..763f6d622da 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -835,6 +835,8 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en htSupportedChannelWidths.insert(bandwidth); if (entry.mode->isHtShortGuardInterval()) htShortGuardIntervalChannelWidths.insert(bandwidth); + if (entry.mode->isHtGreenfield()) + htGreenfieldSupported = true; } if (htOperationSupported) { for (int mcsIndex = 0; mcsIndex < 8; mcsIndex++) @@ -1259,6 +1261,14 @@ const IIeee80211Mode *Ieee80211ModeSet::getNonHtControlResponseMode(const IIeee8 return result; } +const IIeee80211Mode *Ieee80211ModeSet::findHtMixedMode(const IIeee80211Mode *mode) const +{ + if (mode == nullptr) + return nullptr; + auto it = htMixedControlResponseModes.find(mode); + return it != htMixedControlResponseModes.end() ? it->second : nullptr; +} + const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) { for (size_t index = 0; index < (&modeSets)->size(); index++) { diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index 5061b1a827e..ebf96321679 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -51,6 +51,7 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject std::array htMcsMandatory = {}; std::set htSupportedChannelWidths; std::set htShortGuardIntervalChannelWidths; + bool htGreenfieldSupported = false; bool htOperationSupported = false; // Entries are selectable modes; supportedEntries also contains immutable PHY capabilities needed for mandatory control responses. const std::vector supportedEntries; @@ -93,6 +94,7 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const std::set& getHtSupportedChannelWidths() const { return htSupportedChannelWidths; } const std::set& getHtShortGuardIntervalChannelWidths() const { return htShortGuardIntervalChannelWidths; } bool isHtShortGuardIntervalSupported(Hz bandwidth) const { return htShortGuardIntervalChannelWidths.count(bandwidth) != 0; } + bool isHtGreenfieldSupported() const { return htGreenfieldSupported; } // containsMode() covers entries selectable as the persistent operating mode // (for example through Ieee80211Transmitter::setMode()). supportsMode() @@ -134,6 +136,8 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject // 2.4 GHz HT modes are converted to non-HT responses. With mandatory=false, // the input bitrate is used as a ceiling for selecting that non-HT mode. const IIeee80211Mode *getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory = true) const; + // Returns the HT-mixed equivalent mode for an HT mode, or nullptr if not an HT mode or not mapped. + const IIeee80211Mode *findHtMixedMode(const IIeee80211Mode *mode) const; static const Ieee80211ModeSet *findModeSet(const char *mode); static const Ieee80211ModeSet *getModeSet(const char *mode); diff --git a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test new file mode 100644 index 00000000000..37040b28817 --- /dev/null +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -0,0 +1,199 @@ +%description: +Verifies bidirectional unicast delivery and dynamic PPDU format selection in a +heterogeneous BSS with one n(greenfield-2.4Ghz) station, an n(mixed-2.4Ghz) +access point, and an n(mixed-2.4Ghz) station. Because the AP is mixed-only, the +Greenfield station must recognize that the peer cannot receive Greenfield PPDUs +and dynamically select the compatible HT-mixed format for uplink transmissions. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtHeterogeneousGreenfieldRuntime { + +static OPP_THREAD_LOCAL int greenfieldStaMixedDataCount = 0; +static OPP_THREAD_LOCAL int greenfieldStaGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int mixedStaDataCount = 0; +static OPP_THREAD_LOCAL int apForwardedDataCount = 0; +static OPP_THREAD_LOCAL int nonHtAckCount = 0; + +class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener +{ + using cListener::finish; + + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + bool isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; + bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + + std::string fullPath = getFullPath(); + bool isHost0 = fullPath.find("host[0]") != std::string::npos; + bool isHost1 = fullPath.find("host[1]") != std::string::npos; + bool isAp = fullPath.find("ap") != std::string::npos; + + if (dynamicPtrCast(macHeader)) { + if (isHost0) { + if (isHtGreenfield) + greenfieldStaGreenfieldDataCount++; + else if (isHtMixed) + greenfieldStaMixedDataCount++; + } + else if (isHost1 && isHtMixed) + mixedStaDataCount++; + else if (isAp && isHtMixed) + apForwardedDataCount++; + } + if (dynamicPtrCast(macHeader) && mode == nullptr) + nonHtAckCount++; + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (greenfieldStaGreenfieldDataCount > 0) + throw cRuntimeError("Greenfield station sent %d HT-Greenfield frame(s) to mixed-only AP", greenfieldStaGreenfieldDataCount); + if (greenfieldStaMixedDataCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected bidirectional HT-mixed data and non-HT ACK exchanges"); + std::cout << "Observed " << greenfieldStaMixedDataCount << " GF-STA HT-mixed data, " + << mixedStaDataCount << " Mixed-STA data, " + << apForwardedDataCount << " AP forwarded data, 0 Greenfield data, and " + << nonHtAckCount << " non-HT ACK response(s).\n"; + } +}; + +Define_Module(HeterogeneousHtCheckingRadio); + +} // namespace Ieee80211HtHeterogeneousGreenfieldRuntime + +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module HeterogeneousHtCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HeterogeneousHtCheckingRadio); +} + +network TestHeterogeneousHtGreenfield +{ + parameters: + int numHosts = default(2); + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[numHosts]: WirelessHost { + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + ap: AccessPoint { + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + } +} + +%inifile: omnetpp.ini + +[General] +network = TestHeterogeneousHtGreenfield +abstract = false +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 600m +**.constraintAreaMaxY = 400m +**.constraintAreaMaxZ = 0m + +**.arp.typename = "GlobalArp" + +*.ap.wlan[*].address = "10:00:00:00:00:00" +*.host[*].**.mgmt.accessPointAddress = "10:00:00:00:00:00" + +*.ap.mobility.typename = "StationaryMobility" +*.ap.mobility.initialX = 250m +*.ap.mobility.initialY = 200m +*.host[*].mobility.typename = "StationaryMobility" +*.host[*].mobility.initialY = 200m +*.host[0].mobility.initialX = 240m +*.host[1].mobility.initialX = 260m + +**.wlan[*].radio.typename = "HeterogeneousHtCheckingRadio" + +[Config HeterogeneousHtRuntime] +abstract = false +sim-time-limit = 2s +*.numHosts = 2 + +# Node configuration: host[0] is Greenfield, AP and host[1] are Mixed-HT +*.host[0].wlan[*].opMode = "n(greenfield-2.4Ghz)" +*.host[0].wlan[*].bitrate = 13Mbps +*.host[1].wlan[*].opMode = "n(mixed-2.4Ghz)" +*.host[1].wlan[*].bitrate = 13Mbps +*.ap.wlan[*].opMode = "n(mixed-2.4Ghz)" +*.ap.wlan[*].bitrate = 13Mbps + +# Bidirectional UDP traffic between host[0] and host[1] through AP +*.host[0].numApps = 2 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[0].app[0].sendInterval = 100ms + +*.host[0].app[1].typename = "UdpSink" +*.host[0].app[1].localPort = 2000 + +*.host[1].numApps = 2 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 + +*.host[1].app[1].typename = "UdpBasicApp" +*.host[1].app[1].destAddresses = "host[0]" +*.host[1].app[1].destPort = 2000 +*.host[1].app[1].messageLength = 100B +*.host[1].app[1].startTime = 150ms +*.host[1].app[1].sendInterval = 100ms + +%extraargs: -c HeterogeneousHtRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index dee6a669f79..a56cb51a211 100644 --- a/tests/unit/Ieee80211HtCapabilities_1.test +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -20,6 +20,7 @@ local.rxMcsSupported[2] = true; local.rxMcsSupported[3] = true; local.txMcsNss.maxMcsPerNss[0] = 3; local.ldpc = true; +local.greenfield = true; local.shortGi20 = true; local.maxAmpduLengthExponent = 1; @@ -47,6 +48,8 @@ ASSERT(!negotiated.localRxPeerTx.supportedMcs[1]); ASSERT(negotiated.localRxPeerTx.supportedMcs[2]); ASSERT(!negotiated.localTxPeerRx.receiverLdpc); ASSERT(negotiated.localRxPeerTx.receiverLdpc); +ASSERT(!negotiated.localTxPeerRx.receiverGreenfield); +ASSERT(negotiated.localRxPeerTx.receiverGreenfield); ASSERT(negotiated.localTxPeerRx.receiverMaxAmpduLengthExponent == 3); ASSERT(negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent == 1); From 8965217f9a132437d8bca32406705d416c283eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 02:37:23 +0200 Subject: [PATCH 15/23] fix(linklayer): accept supported HT-mixed CTS responses in Greenfield peer mode selection - Rate selection peer filtering: - In selectPeerCompatibleMode(), use modeSet->supportsMode(mode) instead of modeSet->containsMode(mode) to validate candidate modes. - While containsMode() verifies persistent selectable operating modes (which for Greenfield mode sets only include Greenfield PPDUs), supportsMode() correctly covers supported supplementary modes, such as mandatory HT-mixed CTS responses required by IEEE 802.11-2024 subclauses 10.6.6.1 and 10.6.6.5.7. - This prevents runtime simulation aborts when Greenfield stations respond to HT RTS frames in infrastructure networks. - Testing: - Add assertions in tests/unit/Ieee80211PeerModeSelection_1.test verifying that selectPeerCompatibleMode() accepts HT-mixed CTS modes for n(greenfield-2.4Ghz) mode sets while still rejecting unsupported MCS indices. - Update tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test with RTS/CTS enabled (rtsThreshold = 1B) to verify end-to-end HT-mixed CTS generation and transmission by Greenfield stations. --- .../Ieee80211PeerModeSelection.cc | 4 ++-- ...80211HtHeterogeneousGreenfieldRuntime.test | 11 ++++++++--- tests/unit/Ieee80211PeerModeSelection_1.test | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 93fc50b453a..55869e0c04c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -132,8 +132,8 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, return mode; if (modeSet == nullptr) throw cRuntimeError("Cannot select a peer-compatible HT mode without an IEEE 802.11 mode set"); - if (!modeSet->containsMode(mode)) - throw cRuntimeError("HT mode '%s' is not contained in IEEE 802.11 mode set '%s'", + if (!modeSet->supportsMode(mode)) + throw cRuntimeError("HT mode '%s' is not supported by IEEE 802.11 mode set '%s'", mode->getName(), modeSet->getName()); if (peerHtState == nullptr || !peerHtState->valid || !peerHtState->negotiatedCapabilities.localTxPeerRx.valid) diff --git a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test index 37040b28817..e753cf7ce9e 100644 --- a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -22,6 +22,7 @@ namespace Ieee80211HtHeterogeneousGreenfieldRuntime { static OPP_THREAD_LOCAL int greenfieldStaMixedDataCount = 0; static OPP_THREAD_LOCAL int greenfieldStaGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int greenfieldStaMixedCtsCount = 0; static OPP_THREAD_LOCAL int mixedStaDataCount = 0; static OPP_THREAD_LOCAL int apForwardedDataCount = 0; static OPP_THREAD_LOCAL int nonHtAckCount = 0; @@ -72,6 +73,8 @@ class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener else if (isAp && isHtMixed) apForwardedDataCount++; } + if (dynamicPtrCast(macHeader) && isHtMixed && isHost0) + greenfieldStaMixedCtsCount++; if (dynamicPtrCast(macHeader) && mode == nullptr) nonHtAckCount++; } @@ -81,9 +84,10 @@ class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener Ieee80211Radio::finish(); if (greenfieldStaGreenfieldDataCount > 0) throw cRuntimeError("Greenfield station sent %d HT-Greenfield frame(s) to mixed-only AP", greenfieldStaGreenfieldDataCount); - if (greenfieldStaMixedDataCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) - throw cRuntimeError("Expected bidirectional HT-mixed data and non-HT ACK exchanges"); + if (greenfieldStaMixedDataCount == 0 || greenfieldStaMixedCtsCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected bidirectional HT-mixed data, GF-STA HT-mixed CTS responses, and non-HT ACK exchanges"); std::cout << "Observed " << greenfieldStaMixedDataCount << " GF-STA HT-mixed data, " + << greenfieldStaMixedCtsCount << " GF-STA HT-mixed CTS, " << mixedStaDataCount << " Mixed-STA data, " << apForwardedDataCount << " AP forwarded data, 0 Greenfield data, and " << nonHtAckCount << " non-HT ACK response(s).\n"; @@ -156,6 +160,7 @@ seed-set = 0 *.host[1].mobility.initialX = 260m **.wlan[*].radio.typename = "HeterogeneousHtCheckingRadio" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B [Config HeterogeneousHtRuntime] abstract = false @@ -196,4 +201,4 @@ sim-time-limit = 2s %extraargs: -c HeterogeneousHtRuntime %contains-regex: stdout -Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). +Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* GF-STA HT-mixed CTS, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index 6d2b7fbe770..18d18f39f51 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -172,6 +172,25 @@ ASSERT(firstSelection == secondSelection); ASSERT(firstSelection->getHtMcsIndex() == 8); ASSERT(!firstSelection->isHtShortGuardInterval()); +// Greenfield mode set supports HT-mixed control responses (e.g. CTS) per +// IEEE 802.11-2024 10.6.6.5.7 even though HT-mixed modes are not selectable entries. +const auto *greenfieldModeSet = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"); +const auto *htMixedCtsMode = mcs0Long; // htMcs0BW20MHz mixed format +ASSERT(!greenfieldModeSet->containsMode(htMixedCtsMode)); +ASSERT(greenfieldModeSet->supportsMode(htMixedCtsMode)); +auto greenfieldPeer = makePeerState({0}, {MHz(20)}, MHz(20)); +const auto *greenfieldCtsSelection = selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, htMixedCtsMode, peer); +ASSERT(greenfieldCtsSelection == htMixedCtsMode); + +bool outsideGreenfieldModeRejected = false; +try { + selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, modeOutsideSet, peer); +} +catch (const cRuntimeError&) { + outsideGreenfieldModeRejected = true; +} +ASSERT(outsideGreenfieldModeRejected); + EV << "Peer-compatible HT mode selection checks verified.\n"; %contains: stdout From 86dd5f46899cd1c7c3d963e86ced4ec464c937d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 13:11:49 +0200 Subject: [PATCH 16/23] test(linklayer): add unit tests for Greenfield receiver capability and HT-mixed fallback - Peer mode selection testing: - Add optional greenfield receiver capability parameter to makePeerState() helper in Ieee80211PeerModeSelection_1.test. - Verify selectPeerCompatibleMode() selects HT-Greenfield modes when communicating with a Greenfield-capable peer. - Verify isCompatibleHtMode() rejects HT-Greenfield modes when the destination station did not advertise Greenfield reception, causing selectPeerCompatibleMode() to fall back to the compatible HT-mixed equivalent mode. - Verify candidate HT-mixed modes remain unaffected regardless of the destination station's Greenfield reception capability. --- tests/unit/Ieee80211PeerModeSelection_1.test | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index 18d18f39f51..f1fe6b04d88 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -28,7 +28,7 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs } static Ieee80211Mib::PeerHtState makePeerState(std::initializer_list mcsIndexes, - std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false) + std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false, bool greenfield = false) { Ieee80211Mib::PeerHtState state; state.valid = true; @@ -40,6 +40,7 @@ static Ieee80211Mib::PeerHtState makePeerState(std::initializer_list mcsInd receiverCapabilities.supportedChannelWidths.insert(bandwidth); receiverCapabilities.receiverShortGi20 = shortGi20; receiverCapabilities.receiverShortGi40 = shortGi40; + receiverCapabilities.receiverGreenfield = greenfield; state.negotiatedCapabilities.operation.operatingChannelWidth = operatingChannelWidth; return state; } @@ -191,6 +192,28 @@ catch (const cRuntimeError&) { } ASSERT(outsideGreenfieldModeRejected); +const auto *mcs0Greenfield = findHtMode(greenfieldModeSet, 0, MHz(20), Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(mcs0Greenfield != nullptr); +ASSERT(mcs0Greenfield->isHtGreenfield()); + +// When destination advertises Greenfield reception, HT-Greenfield candidate mode is selected. +auto greenfieldCapablePeer = makePeerState({0}, {MHz(20)}, MHz(20), false, false, true); +const auto *greenfieldSelection = selectPeerCompatibleMode(greenfieldModeSet, &greenfieldCapablePeer, mcs0Greenfield, peer); +ASSERT(greenfieldSelection == mcs0Greenfield); +ASSERT(greenfieldSelection->isHtGreenfield()); + +// When destination did not advertise Greenfield reception, isCompatibleHtMode rejects Greenfield mode, +// falling back to the legal HT-mixed equivalent mode. +auto mixedOnlyDestinationPeer = makePeerState({0}, {MHz(20)}, MHz(20), false, false, false); +const auto *mixedFallbackSelection = selectPeerCompatibleMode(greenfieldModeSet, &mixedOnlyDestinationPeer, mcs0Greenfield, peer); +ASSERT(mixedFallbackSelection == htMixedCtsMode); +ASSERT(!mixedFallbackSelection->isHtGreenfield()); + +// HT-mixed mode candidates remain unaffected regardless of Greenfield receiver capability. +const auto *mixedCandidateSelection = selectPeerCompatibleMode(modeSet, &mixedOnlyDestinationPeer, mcs0Long, peer); +ASSERT(mixedCandidateSelection == mcs0Long); +ASSERT(!mixedCandidateSelection->isHtGreenfield()); + EV << "Peer-compatible HT mode selection checks verified.\n"; %contains: stdout From e3d24818500771ee37fe82292bd5941bcd1f6218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 13:58:06 +0200 Subject: [PATCH 17/23] fix(physicallayer): fix transmission duration decomposition for Short GI HT and VHT modes - Physical layer transmission duration decomposition: - In Ieee80211Transmitter::createTransmission(), compute dataDuration using transmissionMode->getDataDuration(B(phyHeader->getLengthField())) instead of raw transmissionMode->getDataMode()->getDuration(...). - For HT mixed format and VHT modes configured with Short Guard Interval (Short GI / 400 ns), standard IEEE 802.11 symbol-boundary rounding (IEEE Std 802.11-2024, Eq. 19-90 and Eq. 21-109) rounds data airtime up to the 4 us symbol boundary. - Comparing duration == preambleDuration + dataDuration now correctly evaluates to true for HT/VHT Short GI transmissions, setting headerDuration to zero (since SIG fields are included in the preamble) and preventing negative dataDuration calculation and runtime simulation errors. - For legacy PHY modes with standalone PLCP headers (OFDM, ERP, DSSS, HR-DSSS), duration == preambleDuration + dataDuration remains false, preserving the header duration allocation. - Testing: - Update assertTransmitterDurationDecomposition() helper in tests/unit/Ieee80211HtGreenfield_1.test to verify duration decomposition with getDataDuration() and accept zero-header HT/VHT modes. - Add test assertions covering VHT Short GI, HT Mixed Short GI, and HT Greenfield Short GI modes with both 0-byte and 64-byte payloads. --- .../packetlevel/Ieee80211Transmitter.cc | 7 ++--- tests/unit/Ieee80211HtGreenfield_1.test | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 8ab34442799..2be14391371 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -167,14 +167,13 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); - const simtime_t modeledDataDuration = transmissionMode->getDataMode()->getDuration(B(phyHeader->getLengthField())); + const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); // HT/VHT include their SIG fields in the PHY preamble duration, so their - // mode duration is exactly preamble + modeled data. Other PHYs expose a + // mode duration is exactly preamble + data duration. Other PHYs expose a // separate header; their residual data interval may also include a trailing // signal extension (ERP). Keep that extension in chronological data time. - const bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + const bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || preambleDuration + headerDuration + dataDuration != duration) throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); diff --git a/tests/unit/Ieee80211HtGreenfield_1.test b/tests/unit/Ieee80211HtGreenfield_1.test index 11a5efa886f..d6a515de3e8 100644 --- a/tests/unit/Ieee80211HtGreenfield_1.test +++ b/tests/unit/Ieee80211HtGreenfield_1.test @@ -71,19 +71,19 @@ static bool rejectsLegacyConfiguredCts(const Ieee80211ModeSet *modeSet, const II } } -static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode) +static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode, b dataLength = B(0)) { - auto duration = mode->getDuration(B(0)); + auto duration = mode->getDuration(dataLength); auto preambleDuration = mode->getPreambleMode()->getDuration(); - auto modeledDataDuration = mode->getDataMode()->getDuration(B(0)); - bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + auto modeledDataDuration = mode->getDataMode()->getDuration(dataLength); + auto dataDuration = mode->getDataDuration(dataLength); + bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; auto headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : mode->getHeaderMode()->getDuration(); - auto dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; ASSERT(preambleDuration >= SIMTIME_ZERO); ASSERT(headerDuration >= SIMTIME_ZERO); ASSERT(dataDuration >= SIMTIME_ZERO); ASSERT(preambleDuration + headerDuration + dataDuration == duration); - if (dynamic_cast(mode) != nullptr) + if (dynamic_cast(mode) != nullptr || dynamic_cast(mode) != nullptr) ASSERT(headerDuration == SIMTIME_ZERO); if (dynamic_cast(mode) != nullptr) ASSERT(dataDuration > modeledDataDuration); @@ -326,8 +326,21 @@ ASSERT(vhtMixed5GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_5GHZ ASSERT(vhtMixed2GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_2_4GHZ); ASSERT(vhtMixed5GhzMode->getDuration(B(0)) == vhtMixed5GhzMode->getPreambleMode()->getDuration() + vhtMixed5GhzMode->getDataMode()->getDuration(B(0))); ASSERT(vhtMixed5GhzMode->getDataMode()->getDuration(B(0)) >= SIMTIME_ZERO); -assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6))); -assertTransmitterDurationDecomposition(vhtMixed5GhzMode); +auto vhtShortGi5GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6)), B(0)); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6)), B(64)); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode, B(0)); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode, B(64)); +assertTransmitterDurationDecomposition(vhtShortGi5GhzMode, B(0)); +assertTransmitterDurationDecomposition(vhtShortGi5GhzMode, B(64)); +assertTransmitterDurationDecomposition(mixedMode, B(0)); +assertTransmitterDurationDecomposition(mixedMode, B(64)); +assertTransmitterDurationDecomposition(greenfieldMode, B(0)); +assertTransmitterDurationDecomposition(greenfieldMode, B(64)); auto erpMixedProfile = Ieee80211ModeSet::getModeSet("g(mixed)"); auto erpOnlyProfile = Ieee80211ModeSet::getModeSet("g(erp)"); From 67eb64646e52d37ba45b64ef20e7cd211d9fe19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 14:10:05 +0200 Subject: [PATCH 18/23] tests(fingerprint): update baseline for wireless TXOP showcase Update the fingerprint baseline for /showcases/wireless/txop/ (General config, run 0) from d2b6-a5d1/tplx;3a6f-4c28/~tNl to 1ecd-df80/tplx;86dd-208d/~tNl. The simulation trajectory changed due to corrections in IEEE 802.11 High Throughput (HT) physical layer airtime calculations and transmission duration decomposition: 1. Transmission Duration Decomposition (Ieee80211Transmitter): HT/VHT modes integrate their SIG fields into the PHY preamble duration (preambleMode->getDuration()). Previously, an 8 us header duration was queried from getHeaderMode()->getDuration() and subtracted a second time from data airtime. Ieee80211Transmitter now recognizes that HT header duration is contained in the preamble (headerDuration = 0), preserving the full modeled data duration. 2. HT Mixed Short-GI Airtime Rounding (Ieee80211HtMode): Data airtime for mixed-format HT short-GI transmissions is now rounded up to a 4 us symbol boundary per IEEE Std 802.11-2024 Eq. (19-90), while symbol interval lookups and guard intervals reflect normative timing. 3. Control-Response Rate Selection (RateSelection / QosRateSelection): Mandatory mode lookups and response rate fallback for RTS/CTS and Block Ack exchanges now use compliant mandatory modes at or below the target rate. Because txop is the showcase exercising 802.11n HT data (A-MSDU) and control frames (RTS/CTS, AddbaReq, WlanAck), these physical layer airtime corrections modify the transmission boundaries and subsequent contention scheduling into the intended, standard-compliant trajectory. --- tests/fingerprint/showcases.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index ef401f4edb1..5162a8b12f2 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -341,5 +341,5 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb6-43bd/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, d2b6-a5d1/tplx;3a6f-4c28/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, 1ecd-df80/tplx;86dd-208d/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 From da1dff38b6524ad97892f2b1edce38a789a60077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 19/23] physicallayer: preserve scalar signal phase durations Scalar signal models forwarded header and data durations to their base constructor in reverse order. Preserve chronological phase boundaries for both transmission and reception analog models. Add direct TX/RX coverage with distinct preamble, header and data durations. --- .../scalar/ScalarSignalAnalogModel.cc | 2 +- tests/unit/ScalarSignalAnalogModel_1.test | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/unit/ScalarSignalAnalogModel_1.test diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc index 459463d7c98..4d0a86c78c7 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc @@ -12,7 +12,7 @@ namespace inet { namespace physicallayer { ScalarSignalAnalogModel::ScalarSignalAnalogModel(const simtime_t preambleDuration, simtime_t headerDuration, simtime_t dataDuration, Hz centerFrequency, Hz bandwidth, W power) : - NarrowbandSignalAnalogModel(preambleDuration, dataDuration, headerDuration, centerFrequency, bandwidth), + NarrowbandSignalAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, bandwidth), power(power) { } diff --git a/tests/unit/ScalarSignalAnalogModel_1.test b/tests/unit/ScalarSignalAnalogModel_1.test new file mode 100644 index 00000000000..1b7e85c256a --- /dev/null +++ b/tests/unit/ScalarSignalAnalogModel_1.test @@ -0,0 +1,26 @@ +%description: +Scalar transmission and reception analog models preserve distinct chronological +preamble, header, and data durations when forwarding to their shared base. + +%includes: +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarTransmissionAnalogModel.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceptionAnalogModel.h" +using namespace inet; +using namespace inet::physicallayer; + +%activity: +const auto preamble = SimTime(16, SIMTIME_US); +const auto header = SimTime(4, SIMTIME_US); +const auto data = SimTime(120, SIMTIME_US); +ScalarTransmissionAnalogModel tx(preamble, header, data, GHz(2.4), MHz(20), mW(1)); +ScalarReceptionAnalogModel rx(preamble, header, data, GHz(2.4), MHz(20), mW(1)); +for (const ISignalAnalogModel *model : {static_cast(&tx), static_cast(&rx)}) { + ASSERT(model->getPreambleDuration() == preamble); + ASSERT(model->getHeaderDuration() == header); + ASSERT(model->getDataDuration() == data); + ASSERT(model->getDuration() == preamble + header + data); +} +EV << "Scalar TX/RX analog phase durations preserved.\n"; + +%contains: stdout +Scalar TX/RX analog phase durations preserved. From 658306a2052bdfdd2260d0af591fe6342da14761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 20/23] ieee80211: restore the timed HT and VHT header phase Using the full PHY prefix as the preamble left HT and VHT transmissions with no timed header phase. Use the mode's chronological duration accessors while retaining the exact total-duration check. Check positive HT header durations in mixed and Greenfield runtime cases, and verify packet and analog phase boundaries across HT, VHT and legacy catalog variants. --- .../packetlevel/Ieee80211Transmitter.cc | 9 +- .../module/Ieee80211HtGreenfieldRuntime.test | 7 +- tests/module/Ieee80211HtMixedRuntime.test | 7 +- tests/module/Ieee80211TransmissionTiming.test | 106 ++++++++++++++++++ 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 tests/module/Ieee80211TransmissionTiming.test diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 2be14391371..1ba824545ea 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -166,14 +166,9 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); + const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); + const simtime_t headerDuration = transmissionMode->getHeaderDuration(); const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); - // HT/VHT include their SIG fields in the PHY preamble duration, so their - // mode duration is exactly preamble + data duration. Other PHYs expose a - // separate header; their residual data interval may also include a trailing - // signal extension (ERP). Keep that extension in chronological data time. - const bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; - const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || preambleDuration + headerDuration + dataDuration != duration) throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); diff --git a/tests/module/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test index 60d013e74f3..7648df48f17 100644 --- a/tests/module/Ieee80211HtGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -42,9 +42,10 @@ class HtPreambleCheckingRadio : public Ieee80211Radio, public cListener if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) throw cRuntimeError("Invalid preamble/header/data duration decomposition"); - if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || - transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) - throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + if (mode != nullptr && (transmission->getPreambleDuration() != mode->getPreambleDuration() || + transmission->getHeaderDuration() != mode->getHeaderDuration() || transmission->getHeaderDuration() <= SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not preserve the mode's timed header phase"); auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); auto isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; auto isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; diff --git a/tests/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test index cd81007886e..297ae4de410 100644 --- a/tests/module/Ieee80211HtMixedRuntime.test +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -45,9 +45,10 @@ class HtMixedCheckingRadio : public Ieee80211Radio, public cListener if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) throw cRuntimeError("Invalid preamble/header/data duration decomposition"); - if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || - transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) - throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + if (mode != nullptr && (transmission->getPreambleDuration() != mode->getPreambleDuration() || + transmission->getHeaderDuration() != mode->getHeaderDuration() || transmission->getHeaderDuration() <= SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not preserve the mode's timed header phase"); auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; if (dynamicPtrCast(macHeader) && isHtMixed) diff --git a/tests/module/Ieee80211TransmissionTiming.test b/tests/module/Ieee80211TransmissionTiming.test new file mode 100644 index 00000000000..563072ddea0 --- /dev/null +++ b/tests/module/Ieee80211TransmissionTiming.test @@ -0,0 +1,106 @@ +%description: +Verify packet transmitter chronological preamble/header/data durations and analog +model boundaries for HT-mixed, HT-Greenfield, VHT, and legacy modes. + +%file: Test.cc +#include +#include "inet/common/InitStages.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/common/contract/bitlevel/ISignalAnalogModel.h" +using namespace inet; +using namespace inet::physicallayer; +namespace Ieee80211TransmissionTiming { +class TimingRadio : public Ieee80211Radio +{ + public: + void encapsulateForTest(Packet *packet) const { encapsulate(packet); } +}; +Define_Module(TimingRadio); +class TimingProbe : public cSimpleModule +{ + protected: + int numInitStages() const override { return NUM_INIT_STAGES; } + void initialize(int stage) override { + if (stage != INITSTAGE_LAST) + return; + auto radio = check_and_cast(getSimulation()->getModuleByPath("Test.host.wlan[0].radio")); + auto transmitter = check_and_cast(radio->getSubmodule("transmitter")); + int count = 0; + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)", "ac", "a", "b", "g(mixed)"}) { + auto modes = Ieee80211ModeSet::getModeSet(profile); + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + // One antenna suffices; use every one-stream catalog variant. + if (mode->getDataMode()->getNumberOfSpatialStreams() > 1) + continue; + transmitter->setModeSetAndMode(modes, mode); + for (int length : {1, 100, 1500}) { + Packet packet("timing"); + packet.insertAtBack(makeShared(B(length))); + packet.addTag()->setMode(mode); + radio->encapsulateForTest(&packet); + auto transmission = transmitter->createTransmission(radio, &packet, SimTime(1)); + ASSERT(transmission->getPreambleDuration() == mode->getPreambleDuration()); + ASSERT(transmission->getHeaderDuration() == mode->getHeaderDuration()); + ASSERT(transmission->getHeaderDuration() > SIMTIME_ZERO); + ASSERT(transmission->getDataDuration() == mode->getDataDuration(B(length))); + ASSERT(transmission->getDuration() == mode->getDuration(B(length))); + ASSERT(transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() == transmission->getDuration()); + auto analog = transmission->getAnalogModel(); + ASSERT(analog->getPreambleDuration() == transmission->getPreambleDuration()); + ASSERT(analog->getHeaderDuration() == transmission->getHeaderDuration()); + ASSERT(analog->getDataDuration() == transmission->getDataDuration()); + delete transmission; + count++; + } + } + } + ASSERT(count > 100); + std::cout << "HT, VHT, and legacy packet transmission phases verified.\n"; + } +}; +Define_Module(TimingProbe); +} +%file: test.ned +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +module TimingRadio extends Ieee80211Radio +{ + parameters: + @class(TimingRadio); +} +simple TimingProbe +{ + parameters: + @class(TimingProbe); +} +network Test +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: AdhocHost; + probe: TimingProbe; +} +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 +sim-time-limit = 0s +**.host.wlan[0].opMode = "a" +**.host.wlan[0].radio.typename = "TimingRadio" +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 100m +**.constraintAreaMaxY = 100m +**.constraintAreaMaxZ = 0m +%contains: stdout +HT, VHT, and legacy packet transmission phases verified. From 4e2b967a31d72002e4adcd251bbd020007b220e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 21/23] ieee80211: bound mandatory multicast rates by the requested rate Optional multicast requests such as 9 and 18 Mb/s could select the fastest mandatory legacy rate and exceed the requested bitrate. Select the highest mandatory legacy rate within the ceiling, preserving exact mandatory requests. Report an error when mandatory legacy modes exist but none meets the bound. Preserve the request when the mandatory legacy set is empty. Cover optional rates, exact requests and both fallback boundaries. --- .../Ieee80211PeerModeSelection.cc | 7 +++++ .../Ieee80211PeerModeSelection.h | 5 ++++ tests/unit/Ieee80211PeerModeSelection_1.test | 26 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 55869e0c04c..c43806f9f60 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -18,15 +18,22 @@ const IIeee80211Mode *selectGroupAddressedMode(const Ieee80211ModeSet *modeSet, { // IEEE Std 802.11-2024, 10.6.5.1 and 10.6.5.4. The model advertises // mandatory legacy operational modes as its BSS basic legacy rate set. + bool hasMandatoryLegacyMode = false; const IIeee80211Mode *legacyMode = nullptr; for (const auto *candidate : modeSet->getLegacyOperationalModes()) { if (!modeSet->getIsMandatory(candidate)) continue; + hasMandatoryLegacyMode = true; + if (candidate->getDataMode()->getNetBitrate() > requestedMode->getDataMode()->getNetBitrate()) + continue; if (candidate == requestedMode) return candidate; if (legacyMode == nullptr || candidate->getDataMode()->getNetBitrate() > legacyMode->getDataMode()->getNetBitrate()) legacyMode = candidate; } + if (legacyMode == nullptr && hasMandatoryLegacyMode) + throw cRuntimeError("No mandatory legacy mode at or below requested group-addressed mode '%s' in mode set '%s'", + requestedMode->getName(), modeSet->getName()); return legacyMode != nullptr ? legacyMode : requestedMode; } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index e89cf445dd8..d00ea491dbc 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -14,6 +14,11 @@ namespace inet { namespace ieee80211 { +/** + * Selects the fastest mandatory legacy mode at or below the requested bitrate, + * preserving an exact mandatory request. Throws if mandatory legacy modes exist + * but none meets the bound; preserves the request if that mandatory set is empty. + */ INET_API const physicallayer::IIeee80211Mode *selectGroupAddressedMode( const physicallayer::Ieee80211ModeSet *modeSet, const physicallayer::IIeee80211Mode *requestedMode); diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index f1fe6b04d88..9ba90515ba3 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -78,6 +78,32 @@ const auto *modeOutsideSet = Ieee80211HtCompliantModes::getCompliantMode(&Ieee80 Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); ASSERT(!modeSet->containsMode(modeOutsideSet)); +// Group-addressed configured optional rates are upper bounds over the basic +// legacy set (modeled by mandatory legacy operational modes). +for (const char *profile : {"a", "g(mixed)"}) { + const auto *groupSet = Ieee80211ModeSet::getModeSet(profile); + for (int rate : {6, 9, 12, 18, 24}) { + const auto *requested = groupSet->getMode(Mbps(rate)); + auto expected = Mbps(rate == 9 ? 6 : rate == 18 ? 12 : rate); + const auto *selected = selectGroupAddressedMode(groupSet, requested); + ASSERT(selected->getDataMode()->getNetBitrate() == expected); + if (rate != 9 && rate != 18) + ASSERT(selected == requested); + } +} +const auto *legacySet = Ieee80211ModeSet::getModeSet("a"); +const auto *six = legacySet->getMode(Mbps(6)); +const auto *nine = legacySet->getMode(Mbps(9)); +Ieee80211ModeSet aboveBound("aboveBound", {{true, nine, true}}, nine, Ieee80211ModeSet::PhyType::OFDM); +bool groupBoundRejected = false; +try { selectGroupAddressedMode(&aboveBound, six); } +catch (const cRuntimeError& error) { + groupBoundRejected = error.getFormattedMessage().find("No mandatory legacy mode at or below") != std::string::npos; +} +ASSERT(groupBoundRejected); +Ieee80211ModeSet noMandatory("noMandatory", {{false, nine, true}}, nine, Ieee80211ModeSet::PhyType::OFDM); +ASSERT(selectGroupAddressedMode(&noMandatory, nine) == nine); + const MacAddress peer("02:00:00:00:00:01"); auto shortGiPeer = makePeerState({8}, {MHz(20)}, MHz(20), true); From c922a5b560bafa211d7d7159fbd9247d1e83f14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 22/23] ieee80211: require unambiguous configured response modes A response bitrate can identify multiple HT MCS, bandwidth, stream-count and guard-interval combinations. Expose matching qualifiers for DCF/QoS ACK and CTS, plus QoS BlockAck, and reject ambiguous configured matches instead of silently depending on catalog order. Mode-set lookup enforces uniqueness only when requested. Automatic responses and other lookup callers retain their existing behavior. Verify qualified CTS conversion, invalid and automatic configurations, and configured multicast ceilings through both production selectors. --- .../mac/rateselection/QosRateSelection.cc | 10 +- .../mac/rateselection/QosRateSelection.ned | 13 ++ .../mac/rateselection/RateSelection.cc | 8 +- .../mac/rateselection/RateSelection.ned | 10 + .../ieee80211/mode/Ieee80211ModeSet.cc | 24 ++- .../ieee80211/mode/Ieee80211ModeSet.h | 9 +- ...ee80211QualifiedResponseRateSelection.test | 189 ++++++++++++++++++ tests/unit/Ieee80211HtModeSet_1.test | 11 + 8 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 tests/module/Ieee80211QualifiedResponseRateSelection.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index b885503275d..788a6bab198 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -20,12 +20,12 @@ namespace ieee80211 { using namespace inet::physicallayer; -static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, const char *modulePath) { if (bitrate == -1) return nullptr; try { - auto result = modeSet->getMode(bps(bitrate)); + auto result = modeSet->getMode(bps(bitrate), bandwidth, numSpatialStreams, guardInterval, true); if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); return result; @@ -86,11 +86,11 @@ void QosRateSelection::updateModes() double controlFrameBitrate = par("controlFrameBitrate"); auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate), Hz(par("responseBlockAckFrameBandwidth")), par("responseBlockAckFrameNumSpatialStreams"), par("responseBlockAckFrameGuardInterval"), true); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, Hz(par("responseCtsFrameBandwidth")), par("responseCtsFrameNumSpatialStreams"), par("responseCtsFrameGuardInterval"), getFullPath().c_str()); auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); multicastFrameMode = newMulticastFrameMode; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index e3fb6369c95..eb62ded5346 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -27,19 +27,32 @@ simple QosRateSelection extends SimpleModule double multicastFrameBitrate @unit(bps) = default(-1bps); + // Configured response rates must identify one selectable PHY mode. Use the matching + // Bandwidth, NumSpatialStreams, and GuardInterval qualifiers to disambiguate rates. + // Unspecified qualifiers are wildcards; ambiguous matches are rejected. Qualifiers + // are ignored for an automatic (-1bps) response. GI must match the modeled mode. // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + double responseAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseAckFrameNumSpatialStreams = default(-1); + double responseAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT BlockAck PPDU. double responseBlockAckFrameBitrate @unit(bps) = default(-1bps); + double responseBlockAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseBlockAckFrameNumSpatialStreams = default(-1); + double responseBlockAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. A configured value must resolve to a // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); + double responseCtsFrameBandwidth @unit(Hz) = default(nan Hz); + int responseCtsFrameNumSpatialStreams = default(-1); + double responseCtsFrameGuardInterval @unit(s) = default(-1s); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 6094d101bd6..b901e505593 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -24,12 +24,12 @@ namespace ieee80211 { using namespace inet::physicallayer; -static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, const char *modulePath) { if (bitrate == -1) return nullptr; try { - auto result = modeSet->getMode(bps(bitrate)); + auto result = modeSet->getMode(bps(bitrate), bandwidth, numSpatialStreams, guardInterval, true); if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); return result; @@ -106,9 +106,9 @@ void RateSelection::updateModes() double controlFrameBitrate = par("controlFrameBitrate"); auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, Hz(par("responseCtsFrameBandwidth")), par("responseCtsFrameNumSpatialStreams"), par("responseCtsFrameGuardInterval"), getFullPath().c_str()); auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); // Commit only after every configured mode has been resolved, so a failed diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 60ae6786a07..0943cf6bed3 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -23,15 +23,25 @@ simple RateSelection extends SimpleModule like IRateSelection double multicastFrameBitrate @unit(bps) = default(-1bps); + // Configured response rates must identify one selectable PHY mode. Use the matching + // Bandwidth, NumSpatialStreams, and GuardInterval qualifiers to disambiguate rates. + // Unspecified qualifiers are wildcards; ambiguous matches are rejected. Qualifiers + // are ignored for an automatic (-1bps) response. GI must match the modeled mode. // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + double responseAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseAckFrameNumSpatialStreams = default(-1); + double responseAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. A configured value must resolve to a // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); + double responseCtsFrameBandwidth @unit(Hz) = default(nan Hz); + int responseCtsFrameNumSpatialStreams = default(-1); + double responseCtsFrameGuardInterval @unit(s) = default(-1s); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 763f6d622da..49f00bec69a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -1040,13 +1040,14 @@ const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode return nullptr; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); + return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval, requireUnique); } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { + const IIeee80211Mode *result = nullptr; for (size_t index = 0; index < entries.size(); index++) { auto mode = entries[index].mode; auto dataMode = mode->getDataMode(); @@ -1058,15 +1059,20 @@ const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams) && guardIntervalMatches) { - return entries[index].mode; + if (!requireUnique) + return mode; + if (result != nullptr && result != mode) + throw cRuntimeError("Ambiguous mode for bitrate range (%g - %g) bps in operation mode '%s'; specify bandwidth, number of spatial streams, and guard interval", + minBitrate.get(), maxBitrate.get(), getName()); + result = mode; } } - return nullptr; + return result; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); + const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval, requireUnique); if (mode == nullptr) throw cRuntimeError("Unknown mode for bitrate %g bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", bitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); @@ -1074,9 +1080,9 @@ const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int n return mode; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval); + const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval, requireUnique); if (mode == nullptr) throw cRuntimeError("Unknown mode for bitrate range (%g - %g) bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", minBitrate.get(), maxBitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index ebf96321679..d3218205b9c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -111,10 +111,11 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject // Pointer lookup is intentionally strict. Use getControlResponseMode() for an explicitly requested response that needs HT-mixed translation. const IIeee80211Mode *findMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getMode(const IIeee80211Mode *mode) const; - const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + // requireUnique rejects multiple matching entries instead of selecting the first. + const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; const IIeee80211Mode *getSlowestMode() const; const IIeee80211Mode *getFastestMode() const; const IIeee80211Mode *getSlowerMode(const IIeee80211Mode *mode) const; diff --git a/tests/module/Ieee80211QualifiedResponseRateSelection.test b/tests/module/Ieee80211QualifiedResponseRateSelection.test new file mode 100644 index 00000000000..61c17a11672 --- /dev/null +++ b/tests/module/Ieee80211QualifiedResponseRateSelection.test @@ -0,0 +1,189 @@ +%description: +Exercise production DCF/QoS configured response resolution, ambiguity rejection, +qualifier preservation for HT-mixed CTS, and optional multicast rate ceilings. + +%file: Test.cc +#include +#include +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211QualifiedResponseRateSelection { + +template +static void checkSelector(Selector& selector, bool qos) +{ + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)"}) { + auto modes = Ieee80211ModeSet::getModeSet(profile); + for (const char *kind : {"Ack", "Cts", "BlockAck"}) { + if (!qos && std::string(kind) == "BlockAck") + continue; + std::string stem = std::string("response") + kind + "Frame"; + auto bitrate = stem + "Bitrate"; + auto bandwidth = stem + "Bandwidth"; + auto streams = stem + "NumSpatialStreams"; + auto gi = stem + "GuardInterval"; + selector.par(bitrate.c_str()).setDoubleValue(13e6); + bool rejected = false; + try { selector.applyModeSet(modes); } + catch (const cRuntimeError& error) { + rejected = error.getFormattedMessage().find("Ambiguous mode") != std::string::npos; + } + ASSERT(rejected); + selector.par(bandwidth.c_str()).setDoubleValue(20e6); + selector.par(gi.c_str()).setDoubleValue(800e-9); + for (int nss : {1, 2}) { + selector.par(streams.c_str()).setIntValue(nss); + selector.applyModeSet(modes); + auto configured = selector.configured(kind); + ASSERT(configured->getHtMcsIndex() == (nss == 1 ? 1 : 8)); + ASSERT(configured->getDataMode()->getBandwidth() == MHz(20)); + ASSERT(configured->getDataMode()->getGuardInterval() == SimTime(800, SIMTIME_NS)); + if (std::string(kind) == "Cts") { + Packet rts("rts"); + rts.addTag()->setMode(configured); + auto response = selector.computeResponseCtsFrameMode(&rts, nullptr); + ASSERT(!response->isHtGreenfield()); + ASSERT(response->getHtMcsIndex() == configured->getHtMcsIndex()); + ASSERT(response->getDataMode()->getBandwidth() == configured->getDataMode()->getBandwidth()); + ASSERT(response->getDataMode()->getNumberOfSpatialStreams() == nss); + ASSERT(response->getDataMode()->getGuardInterval() == configured->getDataMode()->getGuardInterval()); + } + } + // A conflicting explicit GI must not silently choose another tuple. + selector.par(gi.c_str()).setDoubleValue(400e-9); + rejected = false; + try { selector.applyModeSet(modes); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + // Width and short GI also distinguish a configured HT tuple. This + // exercises selection only; packet-level HT40 operation is unsupported. + selector.par(bitrate.c_str()).setDoubleValue(15e6); + selector.par(bandwidth.c_str()).setDoubleValue(40e6); + selector.par(streams.c_str()).setIntValue(1); + selector.applyModeSet(modes); + auto wideShort = selector.configured(kind); + ASSERT(wideShort->getHtMcsIndex() == 0); + ASSERT(wideShort->getDataMode()->getBandwidth() == MHz(40)); + ASSERT(wideShort->isHtShortGuardInterval()); + if (std::string(kind) == "Cts") { + Packet rts("wideShortRts"); + rts.addTag()->setMode(wideShort); + auto response = selector.computeResponseCtsFrameMode(&rts, nullptr); + ASSERT(!response->isHtGreenfield()); + ASSERT(response->getHtMcsIndex() == 0); + ASSERT(response->getDataMode()->getBandwidth() == MHz(40)); + ASSERT(response->getDataMode()->getNumberOfSpatialStreams() == 1); + ASSERT(response->isHtShortGuardInterval()); + } + selector.par(bitrate.c_str()).setDoubleValue(-1); + selector.applyModeSet(modes); // automatic ignores the qualifiers + ASSERT(selector.configured(kind) == nullptr); + selector.par(bandwidth.c_str()).setDoubleValue(NaN); + selector.par(streams.c_str()).setIntValue(-1); + selector.par(gi.c_str()).setDoubleValue(-1); + } + } + for (const char *profile : {"a", "g(mixed)"}) { + for (int rate : {6, 9, 12, 18, 24}) { + selector.par("multicastFrameBitrate").setDoubleValue(rate * 1e6); + selector.applyModeSet(Ieee80211ModeSet::getModeSet(profile)); + auto header = makeShared(); + header->setReceiverAddress(MacAddress::BROADCAST_ADDRESS); + auto selected = selector.groupMode(header); + ASSERT(selected->getDataMode()->getNetBitrate() == Mbps(rate == 9 ? 6 : rate == 18 ? 12 : rate)); + } + } +} + +class DcfProbe : public RateSelection +{ + public: + const IIeee80211Mode *configured(const char *kind) const { return std::string(kind) == "Ack" ? responseAckFrameMode : responseCtsFrameMode; } + const IIeee80211Mode *groupMode(const Ptr& header) { return computeMode(nullptr, header); } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LINK_LAYER) { + checkSelector(*this, false); + std::cout << "DCF qualified responses and multicast ceilings verified.\n"; + } + } +}; +class QosProbe : public QosRateSelection +{ + public: + const IIeee80211Mode *configured(const char *kind) const { return std::string(kind) == "Ack" ? responseAckFrameMode : std::string(kind) == "Cts" ? responseCtsFrameMode : responseBlockAckFrameMode; } + const IIeee80211Mode *groupMode(const Ptr& header) { return computeMode(nullptr, header, nullptr); } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LINK_LAYER) { + checkSelector(*this, true); + std::cout << "QoS qualified responses and multicast ceilings verified.\n"; + } + } +}; +Define_Module(DcfProbe); +Define_Module(QosProbe); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +simple DcfProbe extends RateSelection +{ + parameters: + @class(DcfProbe); + rateControlModule = ""; + mibModule = ""; + multicastFrameBitrate @mutable; + responseAckFrameBitrate @mutable; + responseAckFrameBandwidth @mutable; + responseAckFrameNumSpatialStreams @mutable; + responseAckFrameGuardInterval @mutable; + responseCtsFrameBitrate @mutable; + responseCtsFrameBandwidth @mutable; + responseCtsFrameNumSpatialStreams @mutable; + responseCtsFrameGuardInterval @mutable; +} +simple QosProbe extends QosRateSelection +{ + parameters: + @class(QosProbe); + rateControlModule = ""; + mibModule = ""; + multicastFrameBitrate @mutable; + responseAckFrameBitrate @mutable; + responseAckFrameBandwidth @mutable; + responseAckFrameNumSpatialStreams @mutable; + responseAckFrameGuardInterval @mutable; + responseCtsFrameBitrate @mutable; + responseCtsFrameBandwidth @mutable; + responseCtsFrameNumSpatialStreams @mutable; + responseCtsFrameGuardInterval @mutable; + responseBlockAckFrameBitrate @mutable; + responseBlockAckFrameBandwidth @mutable; + responseBlockAckFrameNumSpatialStreams @mutable; + responseBlockAckFrameGuardInterval @mutable; +} +network Test +{ + submodules: + dcf: DcfProbe; + qos: QosProbe; +} +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 +%contains: stdout +DCF qualified responses and multicast ceilings verified. +%contains: stdout +QoS qualified responses and multicast ceilings verified. diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 44877fd9d68..67a2ba58f8b 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -43,6 +43,17 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs const auto *htModeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); const auto *vhtOnlyModeSet = Ieee80211ModeSet::getModeSet("ac"); +// 13 Mbps identifies both one-stream MCS 1 and two-stream MCS 8. +bool ambiguousRejected = false; +try { htModeSet->getMode(Mbps(13), MHz(20), -1, SimTime(800, SIMTIME_NS), true); } +catch (const cRuntimeError& error) { + ambiguousRejected = error.getFormattedMessage().find("Ambiguous mode") != std::string::npos; +} +ASSERT(ambiguousRejected); +ASSERT(htModeSet->getMode(Mbps(13), MHz(20), 1, SimTime(800, SIMTIME_NS), true)->getHtMcsIndex() == 1); +ASSERT(htModeSet->getMode(Mbps(13), MHz(20), 2, SimTime(800, SIMTIME_NS), true)->getHtMcsIndex() == 8); +ASSERT(htModeSet->findMode(Mbps(9999), MHz(20), 1, -1, true) == nullptr); + ASSERT(htModeSet->isHtOperationSupported()); ASSERT(!vhtOnlyModeSet->isHtOperationSupported()); From 2782671df42081b5a5e9250d5b8b07d6c7e0ccfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 23/23] ieee80211: document the packet PHY HT40 limitation The HT catalog can represent 40 MHz modes, but the packet-level transmitter and receiver advertise only 20 MHz operation. Clarify that nonzero secondary-channel offsets require PHY width support and are rejected by these providers. --- src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index 70474b3d95b..c8afc96c288 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -22,7 +22,7 @@ simple Ieee80211Mib extends SimpleModule @class(Ieee80211Mib); // Model-backed subset of IEEE Std 802.11-2024 HT capability/operation state; not a full Annex C MIB. int htMaxAmpduLengthExponent = default(0); // maximum received A-MPDU length exponent (0..3) - int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; bounded by the mode set + int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; requires PHY width support. The packet-level Ieee80211Transmitter/Receiver support only 20 MHz and reject nonzero offsets. int htProtectionMode = default(0); // 0=none, 1=nonmember, 2=20 MHz, 3=non-HT mixed displayStringTextFormat = default("Address: {address}{ssidStr}\n{modeStr}{stationTypeStr}{qosStr}{associatedStr}"); @display("i=block/table");