From db566892b7b3944c181205de4c53543cc0dc77ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:24 +0200 Subject: [PATCH 01/17] ieee80211: remove redundant blank lines Remove redundant blank lines from the affected IEEE 802.11 sources so subsequent functional changes contain no incidental whitespace cleanup. --- src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc | 1 - .../ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc | 1 - .../physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc | 1 - .../physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h | 1 - .../physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc | 1 - .../wireless/ieee80211/packetlevel/Ieee80211Radio.cc | 1 - .../wireless/ieee80211/packetlevel/Ieee80211Radio.h | 1 - .../wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc | 1 - 8 files changed, 8 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index e5c8fd008a3..1be1aad1cd9 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -8,7 +8,6 @@ #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include - #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" diff --git a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc index 30eaa47132d..e107d002ea9 100644 --- a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc @@ -331,4 +331,3 @@ Ieee80211LayeredOfdmTransmitter::~Ieee80211LayeredOfdmTransmitter() } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index 319128cce20..d7c108f0373 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -544,4 +544,3 @@ const DI Ieee80211HtmcsTable::htMcs76BW40MHz([](){ return new Ie } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h index 6ca84f780f3..3ef1f42c178 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h @@ -265,4 +265,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..e5736766c5a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -1072,4 +1072,3 @@ const DI Ieee80211VhtmcsTable::vhtMcs9BW160MHzNss8([](){ return } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 56114b6c2c6..026cff806bb 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -334,4 +334,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..754091b7311 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -57,4 +57,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..718645694a0 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -161,4 +161,3 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran } // namespace physicallayer } // namespace inet - From 7df7971e858aa3d59ace138418c4eb05fa74e7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:25 +0200 Subject: [PATCH 02/17] ieee80211: require explicit data-mode guard interval queries Rate selection needs to distinguish modes with the same bitrate and different guard intervals. Require every data mode to state its modeled interval, preserving the negative sentinel for PHYs without one. Document the new requirement for external data-mode implementations. --- WHATSNEW | 6 ++++++ doc/src/migration-guide/index.rst | 14 ++++++++++++++ .../wireless/ieee80211/mode/IIeee80211Mode.h | 3 +++ .../wireless/ieee80211/mode/Ieee80211DsssMode.h | 1 + .../wireless/ieee80211/mode/Ieee80211FhssMode.h | 1 + .../wireless/ieee80211/mode/Ieee80211HrDsssMode.h | 1 + .../wireless/ieee80211/mode/Ieee80211HtMode.cc | 10 ++++++++++ .../wireless/ieee80211/mode/Ieee80211HtMode.h | 1 + .../wireless/ieee80211/mode/Ieee80211IrMode.h | 1 + .../wireless/ieee80211/mode/Ieee80211OfdmMode.h | 1 + .../wireless/ieee80211/mode/Ieee80211VhtMode.h | 1 + 11 files changed, 40 insertions(+) diff --git a/WHATSNEW b/WHATSNEW index 5fdd7668423..39fcf1d1cdf 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -29,6 +29,12 @@ Notable backward incompatible changes are the following: adaptive rate control, either where a station transmits to several peers or where it sends group-addressed traffic. +2. IEEE 802.11 PHY mode API + + External implementations of IIeee80211DataMode must implement the new pure + virtual getGuardInterval() query. Return the modeled guard interval as a + simtime_t, or -1 when the PHY has no guard interval. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 942633d269f..60cbea7b48c 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -4,6 +4,20 @@ Migrating Code from INET 3.x ============================ Release: |release| +Migrating IEEE 802.11 PHY Modes +------------------------------ + +External implementations of ``IIeee80211DataMode`` must now implement the pure +virtual guard-interval query: + +.. code-block:: c++ + + const simtime_t getGuardInterval() const override; + +Return the modeled guard interval in simulation time units. For a PHY without a +guard interval, use an explicit override returning ``-1``. FHSS, DSSS, HR-DSSS, +and IR use this value; OFDM, HT, and VHT return their modeled interval. + Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index aa615b7d19e..4c4d9de27d6 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -44,6 +44,9 @@ class INET_API IIeee80211DataMode : public cObject, public IPrintableObject virtual b getCompleteLength(b dataLength) const = 0; 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 = 0; virtual const IModulation *getModulation() const = 0; virtual int getNumberOfSpatialStreams() const = 0; }; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h index 7b81268c9b7..af511f9a772 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h @@ -64,6 +64,7 @@ class INET_API Ieee80211DsssDataMode : public Ieee80211DsssChunkMode, public IIe public: Ieee80211DsssDataMode(const DpskModulationBase *modulation); + virtual const simtime_t getGuardInterval() const override { return -1; } virtual Hz getBandwidth() const override { return MHz(22); } virtual bps getNetBitrate() const override { return Mbps(1) * modulation->getConstellationSize() / 2; } virtual bps getGrossBitrate() const override { return getNetBitrate(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211FhssMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211FhssMode.h index 39295cf2bd5..2b29fe15539 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211FhssMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211FhssMode.h @@ -58,6 +58,7 @@ class INET_API Ieee80211FhssDataMode : public IIeee80211DataMode public: Ieee80211FhssDataMode(const GfskModulationBase *modulation); + virtual const simtime_t getGuardInterval() const override { return -1; } virtual Hz getBandwidth() const override { return Hz(NaN); } virtual bps getNetBitrate() const override { return Mbps(1) * modulation->getConstellationSize() / 2; } virtual bps getGrossBitrate() const override { return getNetBitrate(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.h index 82324fe4a02..724daa40875 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.h @@ -74,6 +74,7 @@ class INET_API Ieee80211HrDsssDataMode : public IIeee80211DataMode public: Ieee80211HrDsssDataMode(bps bitrate); + virtual const simtime_t getGuardInterval() const override { return -1; } virtual Hz getBandwidth() const override { return MHz(22); } virtual bps getNetBitrate() const override { return bitrate; } virtual bps getGrossBitrate() const override { return bitrate; } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index d7c108f0373..8a1735a0d89 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -239,6 +239,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); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 9184765f083..ed1f365dca3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -218,6 +218,7 @@ 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 getGuardInterval() const override; virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211IrMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211IrMode.h index 86170cfc24b..4479d8e2365 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211IrMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211IrMode.h @@ -66,6 +66,7 @@ class INET_API Ieee80211IrDataMode : public IIeee80211DataMode public: Ieee80211IrDataMode(const PpmModulationBase *modulation); + virtual const simtime_t getGuardInterval() const override { return -1; } virtual Hz getBandwidth() const override { return Hz(NaN); } virtual bps getNetBitrate() const override { return Mbps(1) * modulation->getConstellationSize() / 2; } virtual bps getGrossBitrate() const override { return getNetBitrate(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h index 3ef1f42c178..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(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index aa4aa3639b0..58d8edae803 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h @@ -237,6 +237,7 @@ 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 getGuardInterval() const override { return guardIntervalType == HT_GUARD_INTERVAL_LONG ? getGIDuration() : getShortGIDuration(); } virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; From abc2e3e9a8a55297a812f9e58c96519baebbd956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:25 +0200 Subject: [PATCH 03/17] ieee80211: correct HT MCS 32, 73 and 76 definitions The optional HT table entries encode the wrong stream count or modulation. Use one BPSK stream for MCS 32, 16-QAM on stream 4 for MCS 76 at 20 MHz, and 16-QAM on stream 3 for MCS 73 at 40 MHz. --- WHATSNEW | 3 +++ .../wireless/ieee80211/mode/Ieee80211HtMode.cc | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 39fcf1d1cdf..74ebfe04995 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -73,6 +73,9 @@ Notable backward compatible changes are the following: default. Serializers written against an earlier INET version need no source change. See the migration guide for the new signatures. +5. IEEE 802.11 HT/VHT guard intervals and timing + + HT MCS 32, 73, and 76 definitions are corrected. INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index 8a1735a0d89..b16898b0cdd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -452,7 +452,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));}); @@ -500,7 +502,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));}); @@ -546,7 +550,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));}); From 6b9cf9177c6d7324892e19d5379a6fbf106dd1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:25 +0200 Subject: [PATCH 04/17] ieee80211: distinguish band and preamble in mode caches Band and preamble format affect mode behavior but were absent from cache identity, allowing a request to reuse a mode with different properties. Include both in the HT and VHT keys and reject unsupported VHT greenfield requests before cache lookup. Cover rejection before and after a mixed format request. --- .../ieee80211/mode/Ieee80211HtMode.cc | 4 +- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 2 +- .../ieee80211/mode/Ieee80211VhtMode.cc | 8 ++- .../ieee80211/mode/Ieee80211VhtMode.h | 2 +- tests/unit/Ieee80211HtGuardInterval_1.test | 54 +++++++++++++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 tests/unit/Ieee80211HtGuardInterval_1.test diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index b16898b0cdd..3873d5f0e36 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -336,7 +336,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; @@ -358,7 +358,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; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index ed1f365dca3..cecb01ec0f0 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -463,7 +463,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/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index e5736766c5a..a4150306752 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -672,9 +672,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 +697,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; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index 58d8edae803..4b345b9acbe 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h @@ -680,7 +680,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(); diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test new file mode 100644 index 00000000000..1cec08f954e --- /dev/null +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -0,0 +1,54 @@ +%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/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" +#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: +using namespace inet; +using namespace inet::physicallayer; + +%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); + +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); + +EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; + +%contains: stdout +HT guard interval catalog, timing, and lookup checks passed. From bc01713da422622ab02f109d92ef39e2a1fff5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:25 +0200 Subject: [PATCH 05/17] ieee80211: correct HT/VHT PPDU timing Short GI applies to data symbols, while signal fields retain long-GI timing. Round mixed HT and VHT data airtime to long-symbol boundaries; retain raw short-GI timing for HT greenfield. Expose consistent PPDU phase durations and use them in both transmitter paths so the phases sum to the modeled frame duration. Add the independent VHT timing assertions; catalog-based HT assertions follow with lookup. --- WHATSNEW | 7 +++- .../Ieee80211LayeredOfdmTransmitter.cc | 2 +- .../wireless/ieee80211/mode/IIeee80211Mode.h | 5 +++ .../ieee80211/mode/Ieee80211HtMode.cc | 34 +++++++++++++++---- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 8 +++-- .../ieee80211/mode/Ieee80211VhtMode.cc | 30 ++++++++++++---- .../ieee80211/mode/Ieee80211VhtMode.h | 12 +++++-- .../packetlevel/Ieee80211Transmitter.cc | 6 ++-- tests/unit/Ieee80211HtGuardInterval_1.test | 24 +++++++++++++ 9 files changed, 107 insertions(+), 21 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 74ebfe04995..49dc74b022d 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -75,7 +75,12 @@ Notable backward compatible changes are the following: 5. IEEE 802.11 HT/VHT guard intervals and timing - HT MCS 32, 73, and 76 definitions are corrected. + HT MCS 32, 73, and 76 definitions are corrected. HT/VHT signal fields use + long-GI symbol timing independently of the data GI. Mixed-format HT and VHT + short-GI data airtimes are rounded to 4 us boundaries; HT greenfield retains + its unrounded short-GI airtime. Results and fingerprints may change for + simulations using the affected modes and frame formats. + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc index e107d002ea9..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); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index 4c4d9de27d6..0011d588bad 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -42,6 +42,8 @@ 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 @@ -71,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 3873d5f0e36..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 @@ -295,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) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index cecb01ec0f0..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(); } @@ -219,7 +220,7 @@ class INET_API Ieee80211HtDataMode : public IIeee80211DataMode, public Ieee80211 virtual const Ieee80211Htmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211HtCode *getCode() const { return modulationAndCodingScheme->getCode(); } virtual const simtime_t getGuardInterval() const override; - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + virtual const simtime_t getSymbolInterval() const override { return getDFTPeriod() + getGuardInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; @@ -264,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) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index a4150306752..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) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index 4b345b9acbe..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(); } @@ -238,7 +241,9 @@ class INET_API Ieee80211VhtDataMode : public IIeee80211DataMode, public Ieee8021 virtual const Ieee80211Vhtmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211VhtCode *getCode() const { return modulationAndCodingScheme->getCode(); } virtual const simtime_t getGuardInterval() const override { return guardIntervalType == HT_GUARD_INTERVAL_LONG ? getGIDuration() : getShortGIDuration(); } - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + // 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(); } }; @@ -281,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) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 718645694a0..4c6fff86e46 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -151,9 +151,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); } diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 1cec08f954e..f422352f217 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -47,6 +47,30 @@ catch (cRuntimeError&) { rejectedVhtGreenfieldAfterMixed = true; } ASSERT(rejectedVhtGreenfieldAfterMixed); +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)); EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; From 04a96e20571a6c6cdb8c764476116308483fc32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:26 +0200 Subject: [PATCH 06/17] ieee80211: complete guard-interval mode lookup Equal-rate catalog entries can differ in GI and do not form distinct steps for rate adaptation. Qualify bitrate lookups by GI, retain stable equal-rate ordering, and find adjacent rates by bitrate. Provide complete PHY-tuple compatibility lookup and mandatory-at-or-below selection for consumers. Cover catalog traversal, peer fallback, compatible lookup and timing across the HT/VHT catalogs; document lookup migration. --- WHATSNEW | 5 +- doc/src/migration-guide/index.rst | 6 + .../ieee80211/mode/Ieee80211ModeSet.cc | 125 ++++++--- .../ieee80211/mode/Ieee80211ModeSet.h | 14 +- tests/unit/Ieee80211HtGuardInterval_1.test | 239 ++++++++++++++++++ tests/unit/Ieee80211PeerModeSelection_1.test | 4 + 6 files changed, 357 insertions(+), 36 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 49dc74b022d..18508200a37 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -33,7 +33,10 @@ Notable backward incompatible changes are the following: External implementations of IIeee80211DataMode must implement the new pure virtual getGuardInterval() query. Return the modeled guard interval as a - simtime_t, or -1 when the PHY has no guard interval. + simtime_t, or -1 when the PHY has no guard interval. The bitrate-based + Ieee80211ModeSet getMode() and findMode() overloads also take a trailing, + defaulted guardInterval argument. Ordinary existing calls still compile; + member-function pointers must use the new signature. See the migration guide. Notable backward compatible changes are the following: diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 60cbea7b48c..21f95516375 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -18,6 +18,12 @@ Return the modeled guard interval in simulation time units. For a PHY without a guard interval, use an explicit override returning ``-1``. FHSS, DSSS, HR-DSSS, and IR use this value; OFDM, HT, and VHT return their modeled interval. +The bitrate-based ``Ieee80211ModeSet::getMode()`` and ``findMode()`` overloads +now take a trailing ``simtime_t guardInterval = -1`` argument. Existing ordinary +calls can omit it. Update member-function pointer declarations to include this +argument and supply it when invoking through a pointer. Rebuild external code +against the changed interface. + Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- 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/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index f422352f217..f4e1423490f 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -21,6 +21,31 @@ Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and using namespace inet; using namespace inet::physicallayer; +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 notifyModeSet(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; @@ -34,6 +59,172 @@ catch (cRuntimeError&) { } ASSERT(rejectedVhtGreenfieldBeforeMixed); +const auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +ASSERT(modeSet->getNumModes() == 135); + +TestIeee80211Mgmt mgmt; +mgmt.notifyModeSet(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.notifyModeSet(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); + +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); + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); @@ -47,6 +238,7 @@ 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); @@ -71,6 +263,53 @@ 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)); + EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; 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 b98f44e130450464c0e0789ff9fc898fe3771383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:26 +0200 Subject: [PATCH 07/17] ieee80211: qualify fixed data rates by guard interval A fixed bitrate alone can select the wrong guard interval. Apply the configured dataFrameGuardInterval to interface-wide and per-receiver data mode lookup in both rate selectors. Negative values retain unspecified-GI selection; explicit values must match the modeled PHY interval. --- WHATSNEW | 3 +++ .../ieee80211/mac/rateselection/QosRateSelection.cc | 4 ++-- .../ieee80211/mac/rateselection/QosRateSelection.ned | 5 +++-- .../linklayer/ieee80211/mac/rateselection/RateSelection.cc | 4 ++-- .../linklayer/ieee80211/mac/rateselection/RateSelection.ned | 5 +++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 18508200a37..1073bc8cb80 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -84,6 +84,9 @@ Notable backward compatible changes are the following: its unrounded short-GI airtime. Results and fingerprints may change for simulations using the affected modes and frame formats. + RateSelection and QosRateSelection now accept dataFrameGuardInterval to + qualify a configured dataFrameBitrate. The default, -1s, leaves the GI + unspecified; an explicit value must match the selected mode's modeled GI. INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index add5ec700f1..439cacd40ec 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"); @@ -56,7 +56,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) { 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..1d9aca2c890 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"); @@ -75,7 +75,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) { 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). From 036a54bfa732953802a0e311b4a1c9c8c73dfec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:26 +0200 Subject: [PATCH 08/17] ieee80211: select mandatory response rates by bitrate A response may use a mandatory mode at the same bitrate even when the request mode itself is not mandatory. Select the highest mandatory bitrate at or below the request instead of stepping strictly to a lower rate. --- .../linklayer/ieee80211/mac/rateselection/RateSelection.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 1d9aca2c890..a41845f5fb5 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -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); } } From 9039d75807eed3a4dbd66264a35d931ffdc97b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:26 +0200 Subject: [PATCH 09/17] 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. Update the lan80211ac Ping1/run 0/100s fingerprints in examples.csv from bb14-f903/tplx;538b-6566/~tNl to 8180-0d11/tplx;a5d5-2820/~tNl, and synchronize the matching JSON store entries. The JSON ~tNl entry was already stale (1108-9845); it now matches the same verified run. This change makes the AP forward the broadcast ARP request at the mandatory legacy rate of 24 Mbps instead of the configured VHT rate. At event 41, t=0.006500862657, its airtime changes from 72 us to 44 us; the transmission-end event moves 28 us earlier. IEEE 802.11-2024, 10.6.5.4 requires a non-HT basic rate for this group-addressed frame when the basic legacy rate set is nonempty. Causal verification: restoring only the previous group-rate decision with an interposed selectGroupAddressedMode that returns requestedMode reproduces both original fingerprints over the full 100s run, while keeping the GI and duration fixes active. Thus this rate-selection correction, rather than the GI timing changes, accounts for the new fingerprints. The normal debug run passes the updated CSV expectations. The same basic-rate restriction changes group-addressed traffic in legacy networks configured for 54 Mbps. Update run 0 fingerprints for: - examples/manetrouting/multiradio: SingleRadio and MultiRadio; - showcases/general/pcaprecording: PcapRecording; - showcases/visualizer/canvas/statistic: PacketErrorRate; - showcases/visualizer/canvas/submoduleinfo: MACStates and PacketCounts; - showcases/wireless/analogmodel: Distance; - showcases/wireless/power: General; - showcases/wireless/qos: NonQos and Qos; - tutorials/configurator: Step9 and Step10C. These 12 CSV rows and their 34 matching JSON fingerprints retain their existing simulation limits and non-graphical ingredients. The configured 54 Mbps rate previously bypassed the basic-rate restriction; selecting 24 Mbps instead changes airtime and the subsequent event trajectory. In PacketCounts, the first changed group-rate decision is event 11 at 0.01 s, where the requested 54 Mbps mode becomes 24 Mbps. Causal verification at the final series tip reproduces all 12 changed fingerprints. Bypassing only selectGroupAddressedMode's restriction restores all 12 original baselines while retaining the other changes. The ordinary debug runs pass all 12 updated CSV expectations. This extends the same group-addressed basic-rate correction documented above for Ping1 to the affected legacy configurations. --- .../Ieee80211PeerModeSelection.cc | 16 ++ .../Ieee80211PeerModeSelection.h | 3 + .../mac/rateselection/QosRateSelection.cc | 51 ++----- .../mac/rateselection/RateSelection.cc | 18 +-- tests/fingerprint/examples.csv | 6 +- tests/fingerprint/showcases.csv | 16 +- tests/fingerprint/store.json | 72 ++++----- tests/fingerprint/tutorials.csv | 4 +- tests/module/Ieee80211MixedHtDiscovery_1.test | 141 ++++++++++++++++++ 9 files changed, 228 insertions(+), 99 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 439cacd40ec..fab0cbb566e 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -150,9 +150,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()); @@ -163,46 +169,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 a41845f5fb5..d57a4d0c9dd 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -140,28 +140,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/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 696ba2cb33b..0c6b75202ac 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -377,8 +377,8 @@ /examples/manetrouting/gpsr/, -f omnetpp.ini -c DynamicIPv6 -r 0, 20s, 6299-3bf9/tplx;3617-890d/~tNl;2653-aa28/tyf, PASS, wireless adhoc /examples/manetrouting/gpsr/, -f omnetpp.ini -c DynamicGeneric -r 0, 20s, 732c-c576/tplx;03c2-f3ec/~tNl;fb81-3dac/tyf, PASS, wireless adhoc -/examples/manetrouting/multiradio/, -f omnetpp.ini -c MultiRadio -r 0, 20s, ec17-5cc2/tplx;55f5-0894/~tNl, PASS, wireless adhoc Ipv4 -/examples/manetrouting/multiradio/, -f omnetpp.ini -c SingleRadio -r 0, 20s, 85a0-51b8/tplx;c07a-44e1/~tNl;3aa0-49ed/tyf, PASS, wireless adhoc Ipv4 +/examples/manetrouting/multiradio/, -f omnetpp.ini -c MultiRadio -r 0, 20s, a83a-1ece/tplx;f1d9-19c4/~tNl, PASS, wireless adhoc Ipv4 +/examples/manetrouting/multiradio/, -f omnetpp.ini -c SingleRadio -r 0, 20s, c2bd-7372/tplx;fc98-08d6/~tNl;3aa0-49ed/tyf, PASS, wireless adhoc Ipv4 /examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 17ac-0898/tplx;8bbc-d083/~tNl;fdb7-4ab1/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac /examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 19e2-a165/tplx;d96c-5e41/~tNl;ee03-5334/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac @@ -596,7 +596,7 @@ /examples/wireless/lan80211/, -f omnetpp.ini -c Ping1 -r 0, 25s, 3785-bc39/tplx;18be-f36c/~tNl;c76d-5483/~tND, PASS, wireless Ipv4 # /examples/wireless/lan80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND;0000-0000/tyf, ERROR, wireless # [Config Ping2] # __interactive__ -/examples/wireless/lan80211ac/, -f omnetpp.ini -c Ping1 -r 0, 100s, bb14-f903/tplx;538b-6566/~tNl, PASS, Ipv4 +/examples/wireless/lan80211ac/, -f omnetpp.ini -c Ping1 -r 0, 100s, 8180-0d11/tplx;a5d5-2820/~tNl, PASS, Ipv4 # /examples/wireless/lan80211ac/, -f omnetpp.ini -c Ping2 -r 0, ---100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND;0000-0000/tyf, PASS, # [Config Ping2] # __interactive__ /examples/wireless/layered80211/, -f omnetpp.ini -c LayeredCompliant80211Ping -r 0, 100s, 88dd-4b30/tplx;6264-75f5/~tNl;77ce-e367/~tND;7a22-c289/tyf, PASS, wireless Ipv4 diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index ef401f4edb1..a65ae8506ca 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -6,7 +6,7 @@ # /showcases/general/dynamic/, -f omnetpp.ini -c General -r 0, 100s, aa91-2c95/tplx;0000-0000/~tNl;0000-0000/~tND, PASS, -/showcases/general/pcaprecording/, -f omnetpp.ini -c PcapRecording -r 0, 10s, 40e0-9ac8/tplx;96d0-d13f/~tNl;e062-e9c4/~tND;588a-7dd9/tyf, PASS, wireless EthernetMac Ipv4 +/showcases/general/pcaprecording/, -f omnetpp.ini -c PcapRecording -r 0, 10s, a1b9-ff80/tplx;cb8f-443a/~tNl;63a8-4d3e/~tND;588a-7dd9/tyf, PASS, wireless EthernetMac Ipv4 /showcases/measurement/datarate/, -f omnetpp.ini -c General -r 0, 1s, 7af9-93b4/tplx;2d48-cf54/~tNl;ce00-3055/~tND;c848-3d2a/tyf, PASS, Ipv4 /showcases/measurement/endtoenddelay/, -f omnetpp.ini -c General -r 0, 1s, a6ec-9c0d/tplx;5632-17e6/~tNl;3b24-2f2d/~tND;1479-9b89/tyf, PASS, Ipv4 @@ -127,8 +127,8 @@ /showcases/visualizer/canvas/ieee80211/, -f omnetpp.ini -c VisualizingHandover -r 0, 250s, 0f01-f016/tplx;6ded-d407/~tNl;3268-55d7/~tND;07bb-3973/tyf, PASS, wireless /showcases/visualizer/canvas/ieee80211/, -f omnetpp.ini -c SignalLevels -r 0, 100s, 8782-6f1c/tplx;7a73-4da5/~tNl;483a-1ff7/~tND;c099-e5c5/tyf, PASS, Ipv4 -/showcases/visualizer/canvas/submoduleinfo/, -f omnetpp.ini -c PacketCounts -r 0, 5s, 271c-6821/tplx;3c0c-db82/~tNl;d4d4-a4d5/~tND;b145-6576/tyf, PASS, wireless Ipv4 # VisualizingSubmoduleInformation extended -/showcases/visualizer/canvas/submoduleinfo/, -f omnetpp.ini -c MACStates -r 0, 5s, 271c-6821/tplx;3c0c-db82/~tNl;d4d4-a4d5/~tND;4677-fc97/tyf, PASS, wireless Ipv4 +/showcases/visualizer/canvas/submoduleinfo/, -f omnetpp.ini -c PacketCounts -r 0, 5s, e700-27fb/tplx;f16c-28a3/~tNl;a630-063d/~tND;b145-6576/tyf, PASS, wireless Ipv4 # VisualizingSubmoduleInformation extended +/showcases/visualizer/canvas/submoduleinfo/, -f omnetpp.ini -c MACStates -r 0, 5s, e700-27fb/tplx;f16c-28a3/~tNl;a630-063d/~tND;4677-fc97/tyf, PASS, wireless Ipv4 /showcases/visualizer/canvas/instrumentfigures/, -f omnetpp.ini -c General -r 0, 3s, 6622-adb9/tplx;28ca-9f8c/~tNl;adfa-1bdd/~tND;4266-0d42/tyf, PASS, wireless Ipv4 @@ -177,7 +177,7 @@ /showcases/visualizer/canvas/spectrum/, -f omnetpp.ini -c PowerDensityMap -r 0, 1s, 2622-bc42/tplx;6db5-38b6/~tNl;45da-c896/~tND;88d9-8b3f/tyf, PASS, wireless Ipv4 /showcases/visualizer/canvas/statistic/, -f omnetpp.ini -c PingRtt -r 0, 500s, 1479-6398/tplx;9b09-5004/~tNl;c483-33cf/~tND;71d6-de16/tyf, PASS, wireless Ipv4 -/showcases/visualizer/canvas/statistic/, -f omnetpp.ini -c PacketErrorRate -r 0, 25s, eb0f-ff32/tplx;025d-a600/~tNl;d957-cacd/~tND;0b84-e1e1/tyf, PASS, wireless Ipv4 +/showcases/visualizer/canvas/statistic/, -f omnetpp.ini -c PacketErrorRate -r 0, 25s, 78cf-be58/tplx;a1d6-4bb6/~tNl;c537-1526/~tND;0b84-e1e1/tyf, PASS, wireless Ipv4 /showcases/visualizer/canvas/styling/, -f omnetpp.ini -c Line -r 0, 25s, cb1d-a154/tplx;a565-1a98/~tNl;6950-9c50/~tND;7b44-2fb8/tyf, PASS, wireless Ipv4 /showcases/visualizer/canvas/styling/, -f omnetpp.ini -c Font -r 0, 25s, cb1d-a154/tplx;a565-1a98/~tNl;6950-9c50/~tND;5e90-6a92/tyf, PASS, wireless Ipv4 @@ -196,7 +196,7 @@ /showcases/wireless/aggregation/, -f omnetpp.ini -c VoicePriorityAggregation -r 0, 1s, 1dbe-6672/tplx;a253-dbe3/~tNl;d167-ba3e/~tND;bd88-5ba6/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Routing -r 0, 5s, 56b0-3510/tplx;baeb-0d2c/~tNl;378f-fb30/~tND;00a2-e4ee/tyf, PASS, wireless Ipv4 -/showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;b380-6cd5/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 +/showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 98d8-d5d1/tplx;ebe5-7cf5/~tNl;56b4-f09b/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;643d-41a6/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 /showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, aa2d-5d35/tplx;2094-1f2a/~tNl;1470-1e1b/~tND, PASS, wireless Ipv4 @@ -311,10 +311,10 @@ /showcases/wireless/pathloss/, -f omnetpp.ini -c General -r 0, 100s, 171a-e6eb/tplx;3466-822d/~tNl;5909-6e87/~tND;929d-03d5/tyf, PASS, wireless Ipv4 -/showcases/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 498f-b665/tplx;6f50-5caf/~tNl;0ad4-1089/~tND, PASS, wireless Ipv4 +/showcases/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 21cb-ec85/tplx;30b0-ee3e/~tNl;f19f-589f/~tND, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 37fd-5401/tplx;92f7-198c/~tNl;955c-ee96/~tND;093e-1ca4/tyf, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 1a49-72b3/tplx;e605-d79d/~tNl;10fc-7bb0/~tND;bda9-15d1/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 21a9-2c4f/tplx;7734-7809/~tNl;8a89-483d/~tND;093e-1ca4/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, e96b-3834/tplx;b711-4657/~tNl;a1dc-bbc5/~tND;bda9-15d1/tyf, PASS, wireless Ipv4 /showcases/wireless/ratecontrol/, -f omnetpp.ini -c NoRateControl -r 0, 14s, 7ee9-503a/tplx;0816-e58f/~tNl;648e-6e84/~tND;dad3-7f89/tyf, PASS, wireless Ipv4 /showcases/wireless/ratecontrol/, -f omnetpp.ini -c AarfRateControl -r 0, 12s, a7bc-05bb/tplx;9de0-4dd3/~tNl;1209-101b/~tND;7539-d32d/tyf, PASS, wireless Ipv4 diff --git a/tests/fingerprint/store.json b/tests/fingerprint/store.json index 321b6b5c23a..11d89adc409 100644 --- a/tests/fingerprint/store.json +++ b/tests/fingerprint/store.json @@ -16771,7 +16771,7 @@ "sim_time_limit": "20s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "ec17-5cc2", + "fingerprint": "a83a-1ece", "timestamp": 1681992800.3349502, "itervars": "$repetition==0" }, @@ -16783,7 +16783,7 @@ "sim_time_limit": "20s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "55f5-0894", + "fingerprint": "f1d9-19c4", "timestamp": 1681992800.3352785, "itervars": "$repetition==0" }, @@ -16795,7 +16795,7 @@ "sim_time_limit": "20s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "85a0-51b8", + "fingerprint": "c2bd-7372", "timestamp": 1681992800.3365238, "itervars": "$repetition==0" }, @@ -16819,7 +16819,7 @@ "sim_time_limit": "20s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "c07a-44e1", + "fingerprint": "fc98-08d6", "timestamp": 1681992800.3368874, "itervars": "$repetition==0" }, @@ -35623,7 +35623,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "bb14-f903", + "fingerprint": "8180-0d11", "timestamp": 1681992800.58698, "itervars": "$repetition==0" }, @@ -35635,7 +35635,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "1108-9845", + "fingerprint": "a5d5-2820", "timestamp": 1681992800.5873275, "itervars": "$repetition==0" }, @@ -38179,7 +38179,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "40e0-9ac8", + "fingerprint": "a1b9-ff80", "timestamp": 1681992799.492428, "itervars": "$repetition==0" }, @@ -38203,7 +38203,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "87f0-5b5f", + "fingerprint": "63a8-4d3e", "timestamp": 1681992799.4924438, "itervars": "$repetition==0" }, @@ -38215,7 +38215,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "96d0-d13f", + "fingerprint": "cb8f-443a", "timestamp": 1681992799.4924366, "itervars": "$repetition==0" }, @@ -43147,7 +43147,7 @@ "sim_time_limit": "25s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "eb0f-ff32", + "fingerprint": "78cf-be58", "timestamp": 1681992799.6480415, "itervars": "$repetition==0" }, @@ -43171,7 +43171,7 @@ "sim_time_limit": "25s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "8a0d-832f", + "fingerprint": "c537-1526", "timestamp": 1681992799.6481428, "itervars": "$repetition==0" }, @@ -43183,7 +43183,7 @@ "sim_time_limit": "25s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "025d-a600", + "fingerprint": "a1d6-4bb6", "timestamp": 1681992799.6480927, "itervars": "$repetition==0" }, @@ -43435,7 +43435,7 @@ "sim_time_limit": "5s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "271c-6821", + "fingerprint": "e700-27fb", "timestamp": 1681992799.630568, "itervars": "$repetition==0" }, @@ -43459,7 +43459,7 @@ "sim_time_limit": "5s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "32ff-eec4", + "fingerprint": "a630-063d", "timestamp": 1681992799.6306372, "itervars": "$repetition==0" }, @@ -43471,7 +43471,7 @@ "sim_time_limit": "5s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "3c0c-db82", + "fingerprint": "f16c-28a3", "timestamp": 1681992799.6306028, "itervars": "$repetition==0" }, @@ -43483,7 +43483,7 @@ "sim_time_limit": "5s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "271c-6821", + "fingerprint": "e700-27fb", "timestamp": 1681992799.6302016, "itervars": "$repetition==0" }, @@ -43507,7 +43507,7 @@ "sim_time_limit": "5s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "32ff-eec4", + "fingerprint": "a630-063d", "timestamp": 1681992799.6302688, "itervars": "$repetition==0" }, @@ -43519,7 +43519,7 @@ "sim_time_limit": "5s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "3c0c-db82", + "fingerprint": "f16c-28a3", "timestamp": 1681992799.6302357, "itervars": "$repetition==0" }, @@ -44251,7 +44251,7 @@ "sim_time_limit": "2.5s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "1e75-270e", + "fingerprint": "98d8-d5d1", "timestamp": 1681992799.6558988, "itervars": "$repetition==0" }, @@ -44275,7 +44275,7 @@ "sim_time_limit": "2.5s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "9ccb-a57b", + "fingerprint": "56b4-f09b", "timestamp": 1681992799.6560135, "itervars": "$repetition==0" }, @@ -44287,7 +44287,7 @@ "sim_time_limit": "2.5s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "6d7a-d84c", + "fingerprint": "ebe5-7cf5", "timestamp": 1681992799.6559565, "itervars": "$repetition==0" }, @@ -46267,7 +46267,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "498f-b665", + "fingerprint": "21cb-ec85", "timestamp": 1681992799.6967368, "itervars": "$repetition==0" }, @@ -46279,7 +46279,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "fb79-0fa3", + "fingerprint": "f19f-589f", "timestamp": 1681992799.6968684, "itervars": "$repetition==0" }, @@ -46291,7 +46291,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "6f50-5caf", + "fingerprint": "30b0-ee3e", "timestamp": 1681992799.696803, "itervars": "$repetition==0" }, @@ -46303,7 +46303,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "37fd-5401", + "fingerprint": "21a9-2c4f", "timestamp": 1681992799.6971657, "itervars": "$repetition==0" }, @@ -46327,7 +46327,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "eb84-1083", + "fingerprint": "8a89-483d", "timestamp": 1681992799.697302, "itervars": "$repetition==0" }, @@ -46339,7 +46339,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "1061-2f07", + "fingerprint": "7734-7809", "timestamp": 1681992799.6972342, "itervars": "$repetition==0" }, @@ -46351,7 +46351,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "1a49-72b3", + "fingerprint": "e96b-3834", "timestamp": 1681992799.6976697, "itervars": "$repetition==0" }, @@ -46375,7 +46375,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "83cc-bdf2", + "fingerprint": "a1dc-bbc5", "timestamp": 1681992799.697813, "itervars": "$repetition==0" }, @@ -46387,7 +46387,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "9760-e96a", + "fingerprint": "b711-4657", "timestamp": 1681992799.6977413, "itervars": "$repetition==0" }, @@ -50035,7 +50035,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "71f5-b341", + "fingerprint": "e443-0781", "timestamp": 1681992799.813049, "itervars": "$repetition==0" }, @@ -50059,7 +50059,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "7612-50df", + "fingerprint": "0ea7-62fe", "timestamp": 1681992799.8132813, "itervars": "$repetition==0" }, @@ -50071,7 +50071,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "4a61-20ba", + "fingerprint": "cc4d-d519", "timestamp": 1681992799.8131676, "itervars": "$repetition==0" }, @@ -50803,7 +50803,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "7f72-228e", + "fingerprint": "09ce-6649", "timestamp": 1681992799.808365, "itervars": "$repetition==0" }, @@ -50827,7 +50827,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "c423-d956", + "fingerprint": "1f64-df13", "timestamp": 1681992799.8087497, "itervars": "$repetition==0" }, @@ -50839,7 +50839,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "1374-346a", + "fingerprint": "77af-bd15", "timestamp": 1681992799.8085785, "itervars": "$repetition==0" }, diff --git a/tests/fingerprint/tutorials.csv b/tests/fingerprint/tutorials.csv index 1a1b09eb160..4c9e16ffa2c 100644 --- a/tests/fingerprint/tutorials.csv +++ b/tests/fingerprint/tutorials.csv @@ -12,10 +12,10 @@ /tutorials/configurator/, -f omnetpp.ini -c Step7C -r 0, 500s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND;0000-0000/tyf, PASS, /tutorials/configurator/, -f omnetpp.ini -c Step8A -r 0, 100s, f99c-9e43/tplx;e810-af2b/~tNl;8f87-ee38/~tND;d3f3-ed4c/tyf, PASS, wireless EthernetMac Ipv4 # Step8A extended /tutorials/configurator/, -f omnetpp.ini -c Step8B -r 0, 100s, 6daa-17f7/tplx;e810-af2b/~tNl;e839-e562/~tND;af16-1150/tyf, PASS, wireless EthernetMac Ipv4 -/tutorials/configurator/, -f omnetpp.ini -c Step9 -r 0, 100s, 7f72-228e/tplx;44d7-fbf3/~tNl;48a0-6adb/~tND;c38c-98ef/tyf, PASS, wireless EthernetMac Ipv4 +/tutorials/configurator/, -f omnetpp.ini -c Step9 -r 0, 100s, 09ce-6649/tplx;77af-bd15/~tNl;1f64-df13/~tND;c38c-98ef/tyf, PASS, wireless EthernetMac Ipv4 /tutorials/configurator/, -f omnetpp.ini -c Step10A -r 0, 100s, 4b6f-7cd9/tplx;0000-0000/~tNl;0000-0000/~tND;095d-75bd/tyf, PASS, wireless # Step10A extended /tutorials/configurator/, -f omnetpp.ini -c Step10B -r 0, 100s, 4b6f-7cd9/tplx;0000-0000/~tNl;0000-0000/~tND;4d60-fcf9/tyf, PASS, wireless # Step10B extended -/tutorials/configurator/, -f omnetpp.ini -c Step10C -r 0, 100s, 71f5-b341/tplx;4a61-20ba/~tNl;c614-3445/~tND;f078-56fe/tyf, PASS, wireless Ipv4 +/tutorials/configurator/, -f omnetpp.ini -c Step10C -r 0, 100s, e443-0781/tplx;cc4d-d519/~tNl;0ea7-62fe/~tND;f078-56fe/tyf, PASS, wireless Ipv4 /tutorials/configurator/, -f omnetpp.ini -c Step11A -r 0, 500s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND;0000-0000/tyf, PASS, /tutorials/configurator/, -f omnetpp.ini -c Step11B -r 0, 500s, 49cf-d241/tplx;725c-f075/~tNl;15a3-3f79/~tND;7082-ff49/tyf, PASS, EthernetMac Ipv4 /tutorials/configurator/, -f omnetpp.ini -c Step12 -r 0, 100s, a186-bbc1/tplx;7370-3135/~tNl;1438-e824/~tND;4b03-e5a1/tyf, PASS, wireless EthernetMac Ipv4 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 385e01373a1784867df50264632561f6db7c4ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:26 +0200 Subject: [PATCH 10/17] ieee80211: apply mode-set consumers before notification Runtime catalog changes must refresh the transmitter, receiver, MAC HT capabilities, advertised rates, channel access and rate-selection state before observers see the new mode set. Apply typed consumers once before publishing notification and rebuild configured modes from their qualifiers. Validate compatible or explicitly selected transmitter modes and reject reentrant transitions. Consumer and observer failures are fatal simulation errors; these setters do not offer rollback or continuation after failure. Cover successful transitions, peer state, advertisements, fixed-rate failures and notification failures through both setters. Document the listener interface inherited by external subclasses. --- WHATSNEW | 3 + doc/src/migration-guide/index.rst | 6 + .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 48 ++- .../linklayer/ieee80211/mac/Ieee80211Mac.h | 7 +- .../ieee80211/mac/channelaccess/Dcaf.cc | 12 +- .../ieee80211/mac/channelaccess/Dcaf.h | 4 +- .../ieee80211/mac/channelaccess/Edcaf.cc | 12 +- .../ieee80211/mac/channelaccess/Edcaf.h | 3 + .../ieee80211/mac/common/ModeSetListener.cc | 9 +- .../ieee80211/mac/common/ModeSetListener.h | 7 +- .../mac/ratecontrol/RateControlBase.cc | 14 +- .../mac/ratecontrol/RateControlBase.h | 1 + .../mac/rateselection/QosRateSelection.cc | 69 +++-- .../mac/rateselection/QosRateSelection.h | 5 +- .../mac/rateselection/RateSelection.cc | 54 ++-- .../mac/rateselection/RateSelection.h | 8 +- .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 56 ++-- .../ieee80211/mgmt/Ieee80211MgmtBase.h | 7 +- .../packetlevel/IIeee80211ModeSetListener.h | 34 +++ .../packetlevel/Ieee80211ControlInfo.msg | 6 +- .../ieee80211/packetlevel/Ieee80211Radio.cc | 65 +++- .../ieee80211/packetlevel/Ieee80211Radio.h | 7 + .../ieee80211/packetlevel/Ieee80211Receiver.h | 1 + .../packetlevel/Ieee80211Transmitter.cc | 21 +- .../packetlevel/Ieee80211Transmitter.h | 6 + tests/module/Ieee80211ModeSetFailure_1.test | 116 ++++++++ .../module/Ieee80211ModeSetTransition_1.test | 279 ++++++++++++++++++ tests/unit/Ieee80211HtGuardInterval_1.test | 234 +++++++++++++++ 28 files changed, 975 insertions(+), 119 deletions(-) create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h create mode 100644 tests/module/Ieee80211ModeSetFailure_1.test create mode 100644 tests/module/Ieee80211ModeSetTransition_1.test diff --git a/WHATSNEW b/WHATSNEW index 1073bc8cb80..55033f1e004 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -87,6 +87,9 @@ Notable backward compatible changes are the following: RateSelection and QosRateSelection now accept dataFrameGuardInterval to qualify a configured dataFrameBitrate. The default, -1s, leaves the GI unspecified; an explicit value must match the selected mode's modeled GI. + Mode-set listeners also coordinate runtime mode changes with MAC, + management, and rate selection state. + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 21f95516375..44d84ccb432 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -24,6 +24,12 @@ calls can omit it. Update member-function pointer declarations to include this argument and supply it when invoking through a pointer. Rebuild external code against the changed interface. +``Ieee80211Mac``, ``Ieee80211MgmtBase``, ``RateSelection``, and +``ModeSetListener`` implement ``IIeee80211ModeSetListener``. External subclasses +inherit this interface; adding the base does not itself require a source change. +If a subclass replaces mode-set notification handling, preserve the inherited +state updates required by its base class. + Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index d87f581231f..eda7ba25ea5 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -67,24 +67,12 @@ 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")); 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(); @@ -97,6 +85,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"); @@ -346,6 +352,20 @@ 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)); +} + +void Ieee80211Mac::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateLocalHtCapabilities(); +} + 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..f6d1977b1e3 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,12 @@ 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 void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; protected: FcsMode fcsMode; @@ -65,8 +68,10 @@ 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; using MacProtocolBase::receiveSignal; virtual void configureRadioMode(physicallayer::IRadio::RadioMode radioMode); virtual void configureNetworkInterface() override; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 1f6ea0615cb..9cfb55d8859 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -125,11 +125,15 @@ 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(); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h index 2fab0d312c8..804e9d0093c 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h @@ -19,8 +19,10 @@ namespace ieee80211 { class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener { + public: + 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..18f099c8f9c 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -189,11 +189,15 @@ 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(); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index e737cad9162..8add90acd76 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h @@ -30,6 +30,9 @@ namespace ieee80211 { */ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener { + public: + 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..e1a6ec01da2 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc @@ -23,9 +23,14 @@ 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); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h index 05581613050..d0e5c270d51 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h @@ -10,12 +10,17 @@ #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 void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: physicallayer::Ieee80211ModeSet *modeSet = nullptr; 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 fab0cbb566e..37af3da9f9d 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(); } } @@ -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(); @@ -101,10 +112,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 +129,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"); } @@ -242,11 +249,17 @@ const IIeee80211Mode *QosRateSelection::computeMode(Packet *packet, const Ptr(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); - } +void QosRateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); } void QosRateSelection::frameTransmitted(Packet *packet, const Ptr& header) @@ -257,6 +270,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/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 569ef742d37..3922e34b3ce 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -32,11 +32,13 @@ namespace ieee80211 { */ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListener { + public: + 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 @@ -59,6 +61,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 d57a4d0c9dd..67101aceaab 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)); @@ -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(); @@ -187,11 +197,17 @@ const IIeee80211Mode *RateSelection::computeMode(Packet *packet, const Ptr(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); - } +void RateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); } void RateSelection::frameTransmitted(Packet *packet, const Ptr& header) @@ -221,6 +237,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/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 69441331453..e7d7ffb9009 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,12 @@ 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 void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; @@ -59,6 +64,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/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 3c3bc33903a..28845777bc1 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -43,35 +43,39 @@ 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; } 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..a296b5ecdf2 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,12 @@ 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 void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: // configuration ModuleRefByPar mib; 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..af4fcf4f988 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h @@ -0,0 +1,34 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IIEEE80211MODESETLISTENER_H +#define __INET_IIEEE80211MODESETLISTENER_H + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +class Ieee80211ModeSet; + +/** + * Behavioral consumer of a radio's mode-set changes. The radio applies all + * consumers before publishing modesetChanged, so observers see consistent state. + * Applying a change must not notify observers. Failures are fatal simulation + * errors; partially applied changes are not rolled back. + */ +class INET_API IIeee80211ModeSetListener +{ + public: + virtual ~IIeee80211ModeSetListener() = default; + virtual const Ieee80211ModeSet *getModeSet() const = 0; + virtual void applyModeSet(const Ieee80211ModeSet *modeSet) = 0; +}; + +} // namespace physicallayer +} // namespace inet + +#endif 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 026cff806bb..12b3cc1576f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -7,8 +7,13 @@ #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" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" @@ -58,13 +63,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) @@ -82,13 +91,49 @@ 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; + 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"); + auto transmitter = const_cast(check_and_cast(this->transmitter)); + auto receiver = const_cast(check_and_cast(this->receiver)); + // Apply each behavioral consumer once before notifying observers. + std::vector participants; + 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"); + changingModeSet = true; + if (explicitMode) + transmitter->setModeSetAndMode(modeSet, mode); + else + transmitter->setModeSet(modeSet); + receiver->setModeSet(modeSet); + for (auto participant : participants) + participant->applyModeSet(modeSet); receptionTimer = nullptr; + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); emit(listeningChangedSignal, 0); + changingModeSet = false; + EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; } 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 754091b7311..0fc525cbfeb 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,7 +49,11 @@ class INET_API Ieee80211Radio : public FlatRadioBase public: Ieee80211Radio(); + // Update behavioral consumers before publishing the new mode set. + // Failures are fatal simulation errors; these setters do not roll 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); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); 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.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 4c6fff86e46..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; } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index 05b6f29194c..3ac8ebcea24 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -38,7 +38,13 @@ 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. + 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); virtual void setMode(const IIeee80211Mode *mode); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); diff --git a/tests/module/Ieee80211ModeSetFailure_1.test b/tests/module/Ieee80211ModeSetFailure_1.test new file mode 100644 index 00000000000..448b8da059d --- /dev/null +++ b/tests/module/Ieee80211ModeSetFailure_1.test @@ -0,0 +1,116 @@ +%description: +Invalid fixed rates in either selector and exceptions in either radio notification +terminate the simulation. Both radio setters are exercised in independent runs; +no run catches an error or continues using partially updated state. Reentrant +mode-set changes are rejected while the new state is being published. + +%file: ModeSetFailure.cc +#include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" + +using namespace inet; +using namespace inet::physicallayer; + +class ModeSetFailure : 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, cObject *value, cObject *details) override + { + Enter_Method_Silent(); + if (par("failure").stdstringValue() == "reentrant") + check_and_cast(source)->setModeSet(check_and_cast(value)); + throw cRuntimeError("test observer rejects mode set"); + } + virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override + { + Enter_Method_Silent(); + throw cRuntimeError("test observer rejects listening change"); + } + virtual void handleMessage(cMessage *message) override + { + delete message; + auto radio = check_and_cast(getParentModule()->getSubmodule("ap")->getSubmodule("wlan", 0)->getSubmodule("radio")); + auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); + radio->setMode(ht->getMode(Mbps(24))); + auto failure = par("failure").stdstringValue(); + if (failure == "modeset" || failure == "reentrant") + radio->subscribe(modesetChangedSignal, this); + else if (failure == "listening") + radio->subscribe(IRadio::listeningChangedSignal, this); + std::cout << "Attempting " << failure << " explicit=" << par("explicitMode").boolValue() << "\n"; + if (par("explicitMode")) + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + else + radio->setModeSet(legacy); + throw cRuntimeError("UNEXPECTED successful mode-set change"); + } +}; +Define_Module(ModeSetFailure); + +%file: test.ned +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple ModeSetFailure +{ + parameters: + @class(::ModeSetFailure); + string failure; + bool explicitMode; +} +network ModeSetFailureNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + test: ModeSetFailure; +} + +%inifile: omnetpp.ini +[General] +network = ModeSetFailureNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = false +cmdenv-stop-batch-on-error = false +record-vector-results = false +record-scalar-results = false +*.test.failure = ${failure="dcf","hcf","modeset","listening","reentrant"} +*.test.explicitMode = ${false,true} +**.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 +**.mgmt.ssid = "failure" +**.mgmt.beaconInterval = 1s +**.mac.dcf.rateSelection.dataFrameBitrate = ${failure} == "dcf" ? 65Mbps : -1bps +**.mac.hcf.rateSelection.dataFrameBitrate = ${failure} == "hcf" ? 65Mbps : -1bps +**.rateSelection.dataFrameBandwidth = 20MHz +**.rateSelection.dataFrameNumSpatialStreams = 1 +**.rateSelection.dataFrameGuardInterval = 800ns + +%exitcode: 1 + +%file: check_errors.py +from pathlib import Path +text = Path("test.out").read_text() + Path("test.err").read_text() +for failure in ("dcf", "hcf", "modeset", "listening", "reentrant"): + for explicit in (0, 1): + assert text.count(f"Attempting {failure} explicit={explicit}") == 1 +errors = [line for line in text.splitlines() if " Error:" in line] +assert len(errors) == 10, errors +assert sum("Unknown mode for bitrate" in line for line in errors) == 4, errors +assert sum("test observer rejects mode set" in line for line in errors) == 2, errors +assert sum("test observer rejects listening change" in line for line in errors) == 2, errors +assert sum("Reentrant radio mode-set change" in line for line in errors) == 2, errors +assert "UNEXPECTED successful" not in text + +%postrun-command: python3 check_errors.py diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test new file mode 100644 index 00000000000..bc0487158e1 --- /dev/null +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -0,0 +1,279 @@ +%description: +Exercise both radio mode-set setters with DCF and HCF selectors. Successful +HT/legacy transitions refresh PHY-limited capabilities, actual Beacon bodies, +peer state and adaptive rates before observers run, and notify the radio medium. + +%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/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 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; + 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 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 +{ + int notifications = 0; + const Ieee80211ModeSet *observedModeSet = nullptr; + CachingModeSetObserver cachingObserver; + 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); + 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(); + 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(); + } + + virtual void handleMessage(cMessage *message) override + { + delete message; + auto nic = getParentModule()->getSubmodule("ap", 0)->getSubmodule("wlan", 0); + auto radio = check_and_cast(nic->getSubmodule("radio")); + auto mib = check_and_cast(nic->getSubmodule("mib")); + auto medium = check_and_cast(getParentModule()->getSubmodule("radioMedium")); + auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); + radio->setMode(ht->getMode(Mbps(24))); + cachingObserver.modeSet = ht; + radio->subscribe(modesetChangedSignal, &cachingObserver); + nic->subscribe(modesetChangedSignal, this); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + check(nic, ht, true); + auto mac = nic->getSubmodule("mac"); + auto arf = check_and_cast(mac->getSubmodule("dcf")->getSubmodule("rateControl")); + auto onoe = check_and_cast(mac->getSubmodule("hcf")->getSubmodule("rateControl")); + arf->getRate(peer); + onoe->getRate(peer); + int oldListening = medium->listeningNotifications; + radio->setModeSet(legacy); + check(nic, legacy, false); + ASSERT(legacy->containsMode(arf->getRate(peer))); + ASSERT(legacy->containsMode(onoe->getRate(peer))); + radio->setModeSetAndMode(ht, ht->getMode(Mbps(65), MHz(20), 1, 800e-9)); + check(nic, ht, false); + ASSERT(ht->containsMode(arf->getRate(peer))); + ASSERT(ht->containsMode(onoe->getRate(peer))); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + check(nic, ht, true); + ASSERT(notifications == 2); + ASSERT(medium->listeningNotifications == oldListening + 2); + ASSERT(medium->lastListeningSource == radio); + ASSERT(cachingObserver.modeSet == ht); + ASSERT(observedModeSet == ht); + nic->unsubscribe(modesetChangedSignal, this); + radio->unsubscribe(modesetChangedSignal, &cachingObserver); + std::cout << "Mode-set switching, advertisements, peer state, both selectors, and notifications 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); +} + +module TransitionRadioMedium extends Ieee80211ScalarRadioMedium +{ + parameters: + @class(::TransitionRadioMedium); +} + +simple Ieee80211ModeSetTransitionTest extends SimpleModule +{ + parameters: + @class(::Ieee80211ModeSetTransitionTest); +} + +network TransitionNetwork +{ + submodules: + radioMedium: TransitionRadioMedium; + ap[1]: 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 +*.radioMedium.listeningFilter = true +**.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" + +%contains: stdout +Mode-set switching, advertisements, peer state, both selectors, and notifications verified. diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index f4e1423490f..11e854a6a0c 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -21,6 +21,72 @@ Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and 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 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 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 *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); + } +}; + +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 *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); + } +}; + class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase { protected: @@ -199,6 +265,20 @@ ASSERT(unspecified65->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBa 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); @@ -225,6 +305,28 @@ 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); @@ -310,6 +412,138 @@ 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); + +// 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); + +// 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::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))); +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::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))); +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::notifyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::readModeSet(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::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::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))); +ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; From d400a5a2f544c3ed530fa452dd94a9031f094ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:26 +0200 Subject: [PATCH 11/17] ieee80211: enforce SSID element length bounds SSID elements contain at most 32 octets. Use shared wire helpers to reject overlength values and truncated input consistently across management frame types. Cover zero, one, 32 and 33 octets and truncated payloads. --- .../mgmt/Ieee80211MgmtFrameSerializer.cc | 50 ++++++------ tests/unit/Ieee80211SupportedRates_1.test | 78 +++++++++++++++++++ 2 files changed, 102 insertions(+), 26 deletions(-) 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/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 e4e39e24c47efd46095b897d69728b91da89a246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 13:17:26 +0200 Subject: [PATCH 12/17] ieee80211: mark malformed response AIDs incorrect Invalid wire AIDs in association and reassociation responses should mark the frame incorrect while allowing parsing to finish. Substitute zero and preserve status, rates, trailing elements and stream position. Cover missing markers, out-of-range successful AIDs and nonzero AIDs in unsuccessful responses for both frame types. --- .../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 8ddbf0a9599baa1cef3db21f0d4ea7efd1d3fae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 12 Sep 2026 12:44:14 +0200 Subject: [PATCH 13/17] ieee80211: refresh active contention timing on mode-set changes Active contention retained the old IFS, EIFS and slot duration after a radio mode-set change. Refresh the contention owner through IContention, credit completed whole slots and restart the applicable IFS without redrawing backoff. Preserve the schedule when timing is unchanged and update EDCA grant arbitration before mode-set notifications. Cover DCF and EDCA in IFS, backoff, DEFER, EIFS and unchanged-timing cases, asserting the actual grant time and a single random draw. --- .../ieee80211/mac/channelaccess/Dcaf.cc | 2 + .../ieee80211/mac/channelaccess/Edcaf.cc | 2 + .../ieee80211/mac/contention/Contention.cc | 30 ++++ .../ieee80211/mac/contention/Contention.h | 2 + .../ieee80211/mac/contract/IContention.h | 4 + .../module/Ieee80211ContentionModeSet_1.test | 155 ++++++++++++++++++ 6 files changed, 195 insertions(+) create mode 100644 tests/module/Ieee80211ContentionModeSet_1.test diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 9cfb55d8859..b295f45c983 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -134,6 +134,8 @@ void Dcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) Enter_Method_Silent(); modeSet = const_cast(newModeSet); calculateTimingParameters(); + if (contention != nullptr) + contention->updateTimingParameters(ifs, eifs, slotTime); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index 18f099c8f9c..6a2cdcba2f2 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -198,6 +198,8 @@ void Edcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) Enter_Method_Silent(); modeSet = const_cast(newModeSet); calculateTimingParameters(); + if (contention != nullptr) + contention->updateTimingParameters(ifs, eifs, slotTime); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contention/Contention.cc b/src/inet/linklayer/ieee80211/mac/contention/Contention.cc index 4a1471e28f1..e4339774eca 100644 --- a/src/inet/linklayer/ieee80211/mac/contention/Contention.cc +++ b/src/inet/linklayer/ieee80211/mac/contention/Contention.cc @@ -85,6 +85,36 @@ void Contention::startContention(int cw, simtime_t ifs, simtime_t eifs, simtime_ handleWithFSM(START); } +void Contention::updateTimingParameters(simtime_t ifs, simtime_t eifs, simtime_t slotTime) +{ + Enter_Method_Silent(); + ASSERT(ifs >= 0 && eifs >= 0 && slotTime >= 0); + if (this->ifs == ifs && this->eifs == eifs && this->slotTime == slotTime) + return; + bool activeBackoff = fsm.getState() == IFS_AND_BACKOFF; + bool pendingEifs = endEifsTime > simTime(); + if (activeBackoff && this->slotTime > SIMTIME_ZERO) + computeRemainingBackoffSlots(); + this->ifs = ifs; + this->eifs = eifs; + this->slotTime = slotTime; + // Modeling policy for runtime reconfiguration: restart the applicable IFS + // and any unfinished slot, retaining the remaining integer backoff count. + if (pendingEifs) + endEifsTime = simTime() + eifs; + if (activeBackoff) { + backoffOptimizationDelta = SIMTIME_ZERO; + scheduledTransmissionTime = simTime() + (pendingEifs ? std::max(ifs, eifs) : ifs) + backoffSlots * slotTime; + cancelEvent(startTxEvent); + scheduleAt(scheduledTransmissionTime, startTxEvent); + // Keep EDCA's collision arbitration aligned without publishing an + // intermediate mode-set state or announcing a new random backoff. + callback->expectedChannelAccess(scheduledTransmissionTime); + if (hasGUI()) + updateDisplayString(scheduledTransmissionTime); + } +} + void Contention::handleWithFSM(EventType event) { emit(stateChangedSignal, fsm.getState()); diff --git a/src/inet/linklayer/ieee80211/mac/contention/Contention.h b/src/inet/linklayer/ieee80211/mac/contention/Contention.h index 3042528c88d..33528b753ac 100644 --- a/src/inet/linklayer/ieee80211/mac/contention/Contention.h +++ b/src/inet/linklayer/ieee80211/mac/contention/Contention.h @@ -71,6 +71,8 @@ class INET_API Contention : public SimpleModule, public IContention // TODO also add a switchToReception() method? because switching takes time, so we dont automatically switch to tx after completing a transmission! (as we may want to transmit immediate frames afterwards) virtual void startContention(int cw, simtime_t ifs, simtime_t eifs, simtime_t slotTime, ICallback *callback) override; + virtual void updateTimingParameters(simtime_t ifs, simtime_t eifs, simtime_t slotTime) override; + virtual void mediumStateChanged(bool mediumFree) override; virtual void corruptedFrameReceived() override; virtual bool isContentionInProgress() override { return fsm.getState() != IDLE; } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IContention.h b/src/inet/linklayer/ieee80211/mac/contract/IContention.h index c29703bf6ab..63c2285960a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IContention.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IContention.h @@ -46,6 +46,10 @@ class INET_API IContention virtual ~IContention() {} virtual void startContention(int cw, simtime_t ifs, simtime_t eifs, simtime_t slotTime, ICallback *callback) = 0; + // Runtime timing change: retain completed whole backoff slots and restart the + // applicable IFS with the new timing. An unchanged tuple preserves the schedule. + // Does not draw a new backoff or emit notifications while a mode set is applied. + virtual void updateTimingParameters(simtime_t ifs, simtime_t eifs, simtime_t slotTime) = 0; virtual bool isContentionInProgress() = 0; // notifications diff --git a/tests/module/Ieee80211ContentionModeSet_1.test b/tests/module/Ieee80211ContentionModeSet_1.test new file mode 100644 index 00000000000..af38b0fa192 --- /dev/null +++ b/tests/module/Ieee80211ContentionModeSet_1.test @@ -0,0 +1,155 @@ +%description: +Runtime radio mode-set changes refresh active DCF/EDCA contention, retaining +whole remaining slots without another random draw. Cover IFS, backoff, DEFER, +EIFS and unchanged timing, and observe the actual channel grant time. + +%file: ContentionModeSet.cc +#include "inet/linklayer/ieee80211/mac/contention/Contention.h" +#include "inet/linklayer/ieee80211/mac/contract/IChannelAccess.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class TimingProbeContention : public Contention +{ + public: + int slots() const { return backoffSlots; } + State state() const { return static_cast(fsm.getState()); } + simtime_t slot() const { return slotTime; } + simtime_t interframe() const { return ifs; } + simtime_t extendedInterframe() const { return eifs; } + simtime_t grantTime() const { return startTxEvent->getArrivalTime(); } +}; +Define_Module(TimingProbeContention); + +class ContentionModeSetTest : public cSimpleModule, public IChannelAccess::ICallback, public cListener +{ + TimingProbeContention *contention = nullptr; + IChannelAccess *access = nullptr; + Ieee80211Radio *radio = nullptr; + simtime_t expectedGrant; + simtime_t previousGrant; + int initialSlots = 0; + int draws = 0; + bool switched = false; + std::string phase; + + protected: + virtual void initialize() override { scheduleAt(SimTime(1, SIMTIME_US), new cMessage("start")); } + virtual void receiveSignal(cComponent *, simsignal_t, intval_t, cObject *) override { draws++; } + virtual void handleMessage(cMessage *message) override + { + delete message; + if (contention == nullptr) { + phase = par("phase").stdstringValue(); + auto nic = getParentModule()->getSubmodule("ap")->getSubmodule("wlan", 0); + radio = check_and_cast(nic->getSubmodule("radio")); + auto accessModule = nic->getSubmodule("mac")->getModuleByPath(par("qos").boolValue() ? ".hcf.edca.edcaf[1]" : ".dcf.channelAccess"); + access = check_and_cast(accessModule); + contention = check_and_cast(accessModule->getSubmodule("contention")); + contention->subscribe(IContention::backoffPeriodGeneratedSignal, this); + contention->mediumStateChanged(phase != "defer"); + access->requestChannel(this); + initialSlots = contention->slots(); + ASSERT(initialSlots >= 2); + ASSERT(draws == 1); + if (phase == "eifs") + contention->corruptedFrameReceived(); + previousGrant = contention->grantTime(); + simtime_t delay = SimTime(1, SIMTIME_US); + if (phase == "backoff") + delay = contention->interframe() + contention->slot() * 1.5; + scheduleAfter(delay, new cMessage("switch")); + } + else { + ASSERT(!switched); + ASSERT(contention->state() == (phase == "defer" ? Contention::DEFER : Contention::IFS_AND_BACKOFF)); + const auto *target = Ieee80211ModeSet::getModeSet(phase == "unchanged" ? "g(mixed)" : "g(erp)"); + radio->setModeSetAndMode(target, target->getMode(Mbps(24))); + ASSERT(contention->slot() == target->getSlotTime()); + ASSERT(contention->interframe() == target->getSifsTime() + (par("qos").boolValue() ? 3 : 2) * target->getSlotTime()); + ASSERT(contention->slots() == initialSlots - (phase == "backoff" ? 1 : 0)); + ASSERT(draws == 1); + if (phase == "defer") + contention->mediumStateChanged(true); + auto interval = phase == "eifs" ? contention->extendedInterframe() : contention->interframe(); + expectedGrant = phase == "unchanged" ? previousGrant : simTime() + interval + contention->slots() * target->getSlotTime(); + ASSERT(contention->grantTime() == expectedGrant); + switched = true; + } + } + virtual void channelGranted(IChannelAccess *channelAccess) override + { + Enter_Method_Silent(); + ASSERT(switched); + ASSERT(channelAccess == access); + ASSERT(simTime() == expectedGrant); + ASSERT(draws == 1); + access->releaseChannel(this); + contention->unsubscribe(IContention::backoffPeriodGeneratedSignal, this); + std::cout << "Verified contention phase=" << phase << " qos=" << par("qos").boolValue() << "\n"; + endSimulation(); + } +}; +Define_Module(ContentionModeSetTest); + +%file: test.ned +import inet.linklayer.ieee80211.mac.contention.Contention; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple TimingProbeContention extends Contention +{ + @class(::TimingProbeContention); +} +simple ContentionModeSetTest +{ + parameters: + string phase; + bool qos; + @class(::ContentionModeSetTest); +} +network ContentionModeSetNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + test: ContentionModeSetTest; +} + +%inifile: omnetpp.ini +[General] +network = ContentionModeSetNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.test.phase = ${"ifs","backoff","defer","eifs","unchanged"} +*.test.qos = ${qos=false,true} +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = 24Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].mac.qosStation = ${qos} +**.mgmt.ssid = "contention" +**.mgmt.beaconInterval = 1s +**.contention.typename = "TimingProbeContention" +**.contention.backoffOptimization = false +**.cwMin = 31 +**.cwMax = 1023 + +%file: check_results.py +from pathlib import Path +text = Path("test.out").read_text() +for phase in ("ifs", "backoff", "defer", "eifs", "unchanged"): + for qos in (0, 1): + assert text.count(f"Verified contention phase={phase} qos={qos}") == 1, (phase, qos) + +%postrun-command: python3 check_results.py From be1847e72c37890cccec92c5b978ec8892b95a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 12 Sep 2026 12:44:15 +0200 Subject: [PATCH 14/17] ieee80211: preserve guard-interval absence during mode remapping Compatibility lookup treated an absent guard interval as a wildcard, violating its complete PHY tuple preservation contract. Require both intervals to be absent or their specified values to match. Test mismatched GI presence in both directions with otherwise identical mode tuples. Public findMode lookup retains its wildcard semantics. --- .../ieee80211/mode/Ieee80211ModeSet.cc | 5 ++-- tests/unit/Ieee80211HtGuardInterval_1.test | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index e21dbf9d613..f61b952094b 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -682,9 +682,8 @@ const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode 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) || + // Absence is part of the source tuple, not findMode()'s wildcard. + 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() && diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 11e854a6a0c..7ab5633debd 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -21,6 +21,22 @@ Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and using namespace inet; using namespace inet::physicallayer; +// Same OFDM tuple except for an absent GI, to isolate compatibility semantics. +class AbsentGiDataMode : public Ieee80211OfdmDataMode +{ + public: + AbsentGiDataMode(const Ieee80211OfdmDataMode& source) : Ieee80211OfdmDataMode(source) {} + virtual const simtime_t getGuardInterval() const override { return -1; } +}; + +class AbsentGiMode : public Ieee80211OfdmMode +{ + AbsentGiDataMode data; + public: + AbsentGiMode(const Ieee80211OfdmMode& source) : Ieee80211OfdmMode(source), data(*source.getDataMode()) {} + virtual const Ieee80211OfdmDataMode *getDataMode() const override { return &data; } +}; + class TestIeee80211Transmitter : public Ieee80211Transmitter { public: @@ -295,6 +311,14 @@ 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); +AbsentGiMode absentGiMode(*check_and_cast(aMode)); +Ieee80211ModeSet absentGiSet("absent GI", {{true, &absentGiMode, true}}, &absentGiMode, legacyOfdmModeSet->getPhyType()); +ASSERT(legacyOfdmModeSet->findCompatibleMode(&absentGiMode) == nullptr); +ASSERT(absentGiSet.findCompatibleMode(aMode) == nullptr); +ASSERT(absentGiSet.findCompatibleMode(&absentGiMode) == &absentGiMode); +// Public lookup retains its unspecified-GI wildcard. +ASSERT(legacyOfdmModeSet->findMode(Mbps(6), MHz(20), 1, -1) == aMode); + ASSERT(erpMode->getDataMode()->getNetBitrate() == aMode->getDataMode()->getNetBitrate()); ASSERT(erpMode->getDataMode()->getBandwidth() == aMode->getDataMode()->getBandwidth()); ASSERT(erpMode->getDataMode()->getGuardInterval() == aMode->getDataMode()->getGuardInterval()); From 96e6c07c473971ced0d285d6a47abb529112e222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 12 Sep 2026 12:44:15 +0200 Subject: [PATCH 15/17] ieee80211: validate AP channel operation during mode-set changes Rebuilding HT capabilities restored the configured secondary offset without checking the active band, undoing edge-channel fallback to 20 MHz. Supply the runtime AP band during MIB rebuilding and validate the operation before renegotiating peers against the final state. Exercise HT refresh and legacy-to-HT transitions through the radio, checking peer state and beacon operation fields on edge channels. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 11 +++++- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 9 ++++- .../linklayer/ieee80211/mib/Ieee80211Mib.h | 3 +- .../Ieee80211MgmtApChannelChange_1.test | 36 ++++++++++++++++++- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index eda7ba25ea5..8325bb27836 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -26,6 +26,7 @@ #include "inet/linklayer/ieee80211/mac/contract/IRx.h" #include "inet/linklayer/ieee80211/mac/contract/ITx.h" #include "inet/networklayer/contract/IInterfaceTable.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -90,17 +91,25 @@ void Ieee80211Mac::updateLocalHtCapabilities() int operationalHtSpatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), modeSet->getMaximumNumberOfSpatialStreams()); std::set operationalChannelWidths; + const IIeee80211Band *operationBand = nullptr; 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"); + if (getSimulation()->getContextType() != CTX_INITIALIZE && + mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) { + const auto *channel = transmitter->getChannel(); + if (channel == nullptr || channel->getBand() == nullptr) + throw cRuntimeError("HT operation requires a configured channel and band"); + operationBand = channel->getBand(); + } for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) if (transmitter->isHtChannelWidthSupported(channelWidth) && receiver->isHtChannelWidthSupported(channelWidth)) operationalChannelWidths.insert(channelWidth); } - mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); + mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit, operationBand); } void Ieee80211Mac::initializeRadioMode() diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 1be1aad1cd9..ef1c2aabbc1 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -112,7 +112,8 @@ const Ieee80211HtOperation& Ieee80211Mib::getHtOperation() const } void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, - const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit) + const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit, + const physicallayer::IIeee80211Band *operationBand) { // The radio publishes its initial channel at PHYSICAL_LAYER before the MAC // publishes its mode set at LINK_LAYER. Preserve that independent BSS @@ -181,6 +182,12 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS if (protectionMode < 0 || protectionMode > 3) throw cRuntimeError("htProtectionMode must be between 0 and 3"); htOperation.protectionMode = static_cast(protectionMode); + if (operationBand != nullptr) { + // Validate/fallback before peers see the rebuilt operation. This also + // renegotiates peers once, against the final channel width and offset. + setPrimaryChannel(requirePrimaryChannel(), operationBand); + return; + } for (auto& entry : peerHtStates) { if (entry.second.valid) { entry.second.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index 691257c2f07..b29af3d8355 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -101,7 +101,8 @@ class INET_API Ieee80211Mib : public SimpleModule void releaseAssociationId(const MacAddress& address); void clearAssociationIds(); void updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, - const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit); + const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit, + const physicallayer::IIeee80211Band *operationBand = nullptr); bool isHtOperationSupported() const { return localHtCapabilitiesValid; } bool hasPrimaryChannel() const { return primaryChannelAvailable; } int requirePrimaryChannel() const; diff --git a/tests/module/Ieee80211MgmtApChannelChange_1.test b/tests/module/Ieee80211MgmtApChannelChange_1.test index da3f7bd1429..021fb4f81bb 100644 --- a/tests/module/Ieee80211MgmtApChannelChange_1.test +++ b/tests/module/Ieee80211MgmtApChannelChange_1.test @@ -239,6 +239,19 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule ASSERT(selectedMode->getDataMode()->getBandwidth() == MHz(20)); std::cout << "Dynamic channel change to 11 downgraded to 20 MHz and updated peer state.\n"; + // Reapplying HT must not undo the band-derived fallback, including in + // existing peer state. Exercise the real coordinated radio setter. + auto generation = peerState->generation; + radio->setModeSetAndMode(modeSet, modeSet->getMode(Mbps(24))); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 0); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(20)); + peerState = mib->findPeerHtState(staAddress); + ASSERT(peerState->generation == generation + 1); + ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(20)); + mgmt->emitBeaconNow(); + ASSERT(!mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); + std::cout << "HT mode refresh preserves edge-channel fallback and peer state.\n"; + // 4. Dynamic runtime channel change back to channel index 0 (std channel 1) radio->setChannelNumber(0); @@ -337,6 +350,21 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule std::cout << "Dynamic band change revalidated against new active band.\n"; + // Legacy-to-HT on an edge channel must also apply the configured + // secondary-channel policy before advertising HT operation again. + auto legacy = physicallayer::Ieee80211ModeSet::getModeSet("g(mixed)"); + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + radio->setChannelNumber(2); // last channel in the three-channel test band + radio->setModeSetAndMode(modeSet, modeSet->getMode(Mbps(24))); + ASSERT(mib->getHtOperation().primaryChannel == 2); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 0); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(20)); + mgmt->emitBeaconNow(); + ASSERT(mgmt->sentBeacons.back()->getHtOperation().primaryChannel == 44); + ASSERT(!mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); + radio->setChannelNumber(0); + std::cout << "Legacy-to-HT switch validates the active band before advertising.\n"; + // 7. Band notifications may arrive during initialization before the MAC // publishes whether HT operation is supported. A legacy/non-HT AP must // retain the radio's channel without applying HT-only band validation. @@ -424,7 +452,7 @@ record-scalar-results = false *.ap.wlan[0].radio.transmitter.typename = "TestHt40Transmitter" *.ap.wlan[0].radio.receiver.typename = "TestHt40Receiver" **.wlan[*].opMode = "n(mixed-2.4Ghz)" -**.wlan[*].bitrate = 65Mbps +**.wlan[*].bitrate = 24Mbps **.wlan[*].radio.bandName = "2.4 GHz" **.wlan[*].radio.centerFrequency = 2.4GHz **.mobility.initFromDisplayString = false @@ -457,3 +485,9 @@ Dynamic band change revalidated against new active band. %contains: stdout Non-HT band notification deferred HT-only validation. + +%contains: stdout +HT mode refresh preserves edge-channel fallback and peer state. + +%contains: stdout +Legacy-to-HT switch validates the active band before advertising. From af89be0b7a52b8eaaf7bc598d8aba1846bd60ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 12 Sep 2026 12:44:15 +0200 Subject: [PATCH 16/17] ieee80211: test fatal rejection of unsupported AP band changes The existing invalid-band check called the MIB directly and did not exercise radio band mutation followed by AP validation. Call setBand with a band lacking standard channel mapping and assert the fatal simulation error, without catching it or continuing from partial state. --- tests/module/Ieee80211ModeSetFailure_1.test | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/module/Ieee80211ModeSetFailure_1.test b/tests/module/Ieee80211ModeSetFailure_1.test index 448b8da059d..ecc874efee4 100644 --- a/tests/module/Ieee80211ModeSetFailure_1.test +++ b/tests/module/Ieee80211ModeSetFailure_1.test @@ -1,11 +1,13 @@ %description: Invalid fixed rates in either selector and exceptions in either radio notification terminate the simulation. Both radio setters are exercised in independent runs; -no run catches an error or continues using partially updated state. Reentrant +no run catches an error or continues using partially updated state. A band +without standard channel mapping also fails through the real radio setter. Reentrant mode-set changes are rejected while the new state is being published. %file: ModeSetFailure.cc #include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" using namespace inet; @@ -40,6 +42,10 @@ class ModeSetFailure : public cSimpleModule, public cListener else if (failure == "listening") radio->subscribe(IRadio::listeningChangedSignal, this); std::cout << "Attempting " << failure << " explicit=" << par("explicitMode").boolValue() << "\n"; + if (failure == "band") { + radio->setBand(&Ieee80211CompliantBands::band5GHz); + throw cRuntimeError("UNEXPECTED successful band change"); + } if (par("explicitMode")) radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); else @@ -78,7 +84,7 @@ cmdenv-express-mode = false cmdenv-stop-batch-on-error = false record-vector-results = false record-scalar-results = false -*.test.failure = ${failure="dcf","hcf","modeset","listening","reentrant"} +*.test.failure = ${failure="dcf","hcf","modeset","listening","reentrant","band"} *.test.explicitMode = ${false,true} **.mobility.initFromDisplayString = false **.mobility.initialX = 10m @@ -102,15 +108,16 @@ record-scalar-results = false %file: check_errors.py from pathlib import Path text = Path("test.out").read_text() + Path("test.err").read_text() -for failure in ("dcf", "hcf", "modeset", "listening", "reentrant"): +for failure in ("dcf", "hcf", "modeset", "listening", "reentrant", "band"): for explicit in (0, 1): assert text.count(f"Attempting {failure} explicit={explicit}") == 1 errors = [line for line in text.splitlines() if " Error:" in line] -assert len(errors) == 10, errors +assert len(errors) == 12, errors assert sum("Unknown mode for bitrate" in line for line in errors) == 4, errors assert sum("test observer rejects mode set" in line for line in errors) == 2, errors assert sum("test observer rejects listening change" in line for line in errors) == 2, errors assert sum("Reentrant radio mode-set change" in line for line in errors) == 2, errors +assert sum("Invalid primary channel" in line for line in errors) == 2, errors assert "UNEXPECTED successful" not in text %postrun-command: python3 check_errors.py From aaf3d92a1484f8d577dbc50f5c95b65fbc0836b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 12 Sep 2026 15:21:52 +0200 Subject: [PATCH 17/17] ieee80211: coordinate mode-set transitions through explicit contracts Discovering transition participants through signal subscriptions makes PHY state changes depend on observer attachment and notification scope. Let the interface register consumers explicitly, apply MAC state before derived state, and publish the completed transition from one owner. Validate membership before changing PHY state and retain fatal failure semantics. Expose radio capabilities through a read-only contract so MAC and AP management can validate HT operation without concrete PHY dependencies. Preserve exact configured rates and bound existing contention windows when remapping mode-dependent state. Document initialization, event ownership, and extension migration requirements. Cover registration and lifetime failures, observer independence, initial AP channel validation, generic-radio substitution, and contention timing. Debug and release builds passed, along with 10 module tests, 4 unit tests, and 16 scoped fingerprint cases. Fingerprint baselines are unchanged. Scoped architecture, naming, and seal checks passed; repository-wide gates still report existing violations outside the changed paths. --- WHATSNEW | 14 +- doc/src/migration-guide/index.rst | 35 ++- .../linklayer/ieee80211/Ieee80211Interface.cc | 106 ++++++++ .../linklayer/ieee80211/Ieee80211Interface.h | 38 +++ .../ieee80211/Ieee80211Interface.ned | 6 +- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 28 +-- .../linklayer/ieee80211/mac/Ieee80211Mac.ned | 1 - .../ieee80211/mac/channelaccess/Dcaf.cc | 8 +- .../ieee80211/mac/channelaccess/Edcaf.cc | 8 +- .../ieee80211/mac/common/ModeSetListener.cc | 6 +- .../mac/ratecontrol/RateControlBase.cc | 3 +- .../mac/rateselection/QosRateSelection.cc | 3 +- .../mac/rateselection/RateSelection.cc | 6 +- .../ieee80211/mgmt/Ieee80211MgmtApBase.cc | 13 +- .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 6 +- .../IIeee80211ModeSetCoordinator.h | 40 +++ .../IIeee80211ModeSetCoordinator.ned | 14 ++ .../packetlevel/IIeee80211ModeSetListener.h | 5 +- .../contract/packetlevel/IIeee80211Radio.h | 32 +++ .../contract/packetlevel/IIeee80211Radio.ned | 14 ++ .../ieee80211/mode/Ieee80211ModeSet.cc | 4 +- .../ieee80211/packetlevel/Ieee80211Radio.cc | 33 +-- .../ieee80211/packetlevel/Ieee80211Radio.h | 8 +- .../ieee80211/packetlevel/Ieee80211Radio.ned | 5 +- .../module/Ieee80211ContentionModeSet_1.test | 61 ++++- .../Ieee80211MgmtApUnavailableChannel_1.test | 4 +- tests/module/Ieee80211ModeSetFailure_1.test | 15 +- .../Ieee80211ModeSetRegistration_1.test | 232 ++++++++++++++++++ .../module/Ieee80211ModeSetTransition_1.test | 30 ++- tests/unit/Ieee80211HtGuardInterval_1.test | 6 +- tests/unit/Ieee80211HtModeSet_1.test | 21 ++ 31 files changed, 704 insertions(+), 101 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/Ieee80211Interface.cc create mode 100644 src/inet/linklayer/ieee80211/Ieee80211Interface.h create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.ned create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.ned create mode 100644 tests/module/Ieee80211ModeSetRegistration_1.test diff --git a/WHATSNEW b/WHATSNEW index 55033f1e004..4495828e8c6 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -87,8 +87,18 @@ Notable backward compatible changes are the following: RateSelection and QosRateSelection now accept dataFrameGuardInterval to qualify a configured dataFrameBitrate. The default, -1s, leaves the GI unspecified; an explicit value must match the selected mode's modeled GI. - Mode-set listeners also coordinate runtime mode changes with MAC, - management, and rate selection state. + The interface now coordinates explicitly registered mode-set consumers, + updating MAC, management, rate selection, and active contention before + publishing modesetChanged. Subscribe at the interface; the attached radio + and MAC no longer publish this event. Failures remain fatal. External PHY + and contention implementations have new contracts described in the + migration guide. + + Group-addressed frames use a legacy basic rate when that set is nonempty. + Eligible configured basic rates are preserved; other configured rates fall + back to the fastest legacy basic rate. For example, a configured 54 Mbps + group rate becomes 24 Mbps in the default mixed legacy catalog. This changes + airtime and fingerprints in affected existing scenarios. INET-4.7 (July 2026) — feature release diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 44d84ccb432..b44cf4a6837 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -24,11 +24,38 @@ calls can omit it. Update member-function pointer declarations to include this argument and supply it when invoking through a pointer. Rebuild external code against the changed interface. +``Ieee80211Interface`` now implements ``IIeee80211ModeSetCoordinator``. ``Ieee80211Mac``, ``Ieee80211MgmtBase``, ``RateSelection``, and -``ModeSetListener`` implement ``IIeee80211ModeSetListener``. External subclasses -inherit this interface; adding the base does not itself require a source change. -If a subclass replaces mode-set notification handling, preserve the inherited -state updates required by its base class. +``ModeSetListener`` register their ``IIeee80211ModeSetListener`` contract during +initialization. External subclasses must preserve base initialization and place +mode-set state updates in ``applyModeSet()``, not ``receiveSignal()``. Custom +interface compositions must provide the coordinator contract and register their +consumers explicitly. Register the MAC in ``MAC_STATE`` and independent derived +consumers in ``DERIVED_STATE``. Duplicate registration in the same phase is +idempotent; registration after initial application is rejected. Detach a consumer +before deleting it; membership cannot change during a transition. + +Observe ``modesetChanged`` at the interface. It is published once after initial +link-layer setup, and once after every successful runtime application, including +reapplication of the same catalog. The interface is the source; the MAC and the +attached radio no longer publish this signal. The borrowed mode-set payload is +immutable. Observers do not participate in the transaction. Standalone radios +without ``modeSetCoordinatorModule`` continue to publish their own notification. +Participant and observer failures remain fatal simulation errors; partial changes +are not rolled back and continuation is unsupported. + +HT-capable radio implementations must provide ``IIeee80211Radio`` in addition to +``IRadio``. Its ``isHtChannelWidthSupported()`` query covers both PHY directions, +and ``getChannel()`` returns the borrowed configured channel or null. Generic +legacy radios do not need this contract. AP channel/band validation now precedes +the initial mode-set notification as well as runtime publication. + +External ``IContention`` implementations must implement the new pure virtual +``updateTimingParameters(ifs, eifs, slotTime)`` method. On a runtime timing change, +retain completed whole backoff slots and the remaining random draw, restart the +applicable IFS and any unfinished slot, and update the expected grant time. +Unchanged timing preserves the existing schedule. This application must not emit +an intermediate mode-set notification or generate a new random backoff. Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.cc b/src/inet/linklayer/ieee80211/Ieee80211Interface.cc new file mode 100644 index 00000000000..3d9651e31d5 --- /dev/null +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.cc @@ -0,0 +1,106 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "inet/linklayer/ieee80211/Ieee80211Interface.h" + +#include "inet/common/ModuleAccess.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + +namespace inet { +namespace ieee80211 { + +using namespace inet::physicallayer; + +Define_Module(Ieee80211Interface); + +void Ieee80211Interface::initialize(int stage) +{ + NetworkInterface::initialize(stage); + if (stage == INITSTAGE_LINK_LAYER) { + auto mac = check_and_cast(getSubmodule("mac")); + modeSet = mac->getModeSet(); + beginModeSetChange(modeSet); + completeModeSetChange(modeSet); + modeSetInitialized = true; + } + else if (stage == INITSTAGE_NETWORK_CONFIGURATION) { + // All child link-layer wiring is ready before the initial completed fact. + changingModeSet = true; + modeSetPublished = true; + emit(modesetChangedSignal, const_cast(modeSet)); + changingModeSet = false; + } +} + +void Ieee80211Interface::registerModeSetConsumer(cModule *consumer, Phase phase) +{ + Enter_Method_Silent(); + if (changingModeSet || modeSetInitialized) + throw cRuntimeError("Mode-set consumers must register before initialization completes"); + if (consumer == nullptr || getContainingNicModule(consumer) != this || + dynamic_cast(consumer) == nullptr) + throw cRuntimeError("Mode-set consumer must implement the contract in this interface"); + if (phase != MAC_STATE && phase != DERIVED_STATE) + throw cRuntimeError("Invalid mode-set consumer phase"); + if ((consumer == getSubmodule("mac")) != (phase == MAC_STATE)) + throw cRuntimeError("Only the configured MAC owns the mode-set MAC_STATE phase"); + auto result = modeSetConsumers.emplace(consumer->getId(), phase); + if (!result.second && result.first->second != phase) + throw cRuntimeError("Mode-set consumer registered in two phases"); +} + +void Ieee80211Interface::unregisterModeSetConsumer(cModule *consumer) +{ + Enter_Method_Silent(); + if (changingModeSet) + throw cRuntimeError("Cannot detach a mode-set consumer during a transition"); + if (consumer == nullptr || getContainingNicModule(consumer) != this) + throw cRuntimeError("Cannot detach a mode-set consumer from another interface"); + modeSetConsumers.erase(consumer->getId()); +} + +void Ieee80211Interface::beginModeSetChange(const Ieee80211ModeSet *modeSet) +{ + Enter_Method_Silent(); + if (changingModeSet) + throw cRuntimeError("Reentrant interface mode-set change"); + if (modeSet == nullptr) + throw cRuntimeError("Cannot clear the mode set of an IEEE 802.11 interface"); + for (const char *name : {"mac", "mgmt"}) { + auto module = getSubmodule(name); + if (module == nullptr || modeSetConsumers.count(module->getId()) == 0) + throw cRuntimeError("Required mode-set consumer '%s' is not registered", name); + } + for (const auto& entry : modeSetConsumers) + if (getSimulation()->getModule(entry.first) == nullptr) + throw cRuntimeError("Mode-set consumer was deleted without unregistering"); + pendingModeSet = modeSet; + changingModeSet = true; +} + +void Ieee80211Interface::completeModeSetChange(const Ieee80211ModeSet *modeSet) +{ + Enter_Method_Silent(); + if (!changingModeSet || pendingModeSet != modeSet) + throw cRuntimeError("Mode-set completion does not match the pending transition"); + for (auto phase : {MAC_STATE, DERIVED_STATE}) { + for (const auto& entry : modeSetConsumers) { + if (entry.second != phase) + continue; + auto consumer = getSimulation()->getModule(entry.first); + check_and_cast(consumer)->applyModeSet(modeSet); + } + } + this->modeSet = modeSet; + if (modeSetPublished) + emit(modesetChangedSignal, const_cast(modeSet)); + pendingModeSet = nullptr; + changingModeSet = false; +} + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.h b/src/inet/linklayer/ieee80211/Ieee80211Interface.h new file mode 100644 index 00000000000..c03a4b89bc7 --- /dev/null +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.h @@ -0,0 +1,38 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IEEE80211INTERFACE_H +#define __INET_IEEE80211INTERFACE_H + +#include "inet/networklayer/common/NetworkInterface.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" + +namespace inet { +namespace ieee80211 { + +class INET_API Ieee80211Interface : public NetworkInterface, public physicallayer::IIeee80211ModeSetCoordinator +{ + protected: + std::map modeSetConsumers; + const physicallayer::Ieee80211ModeSet *pendingModeSet = nullptr; + const physicallayer::Ieee80211ModeSet *modeSet = nullptr; + bool changingModeSet = false; + bool modeSetInitialized = false; + bool modeSetPublished = false; + + virtual void initialize(int stage) override; + + public: + virtual void registerModeSetConsumer(cModule *consumer, Phase phase) override; + virtual void unregisterModeSetConsumer(cModule *consumer) override; + virtual void beginModeSetChange(const physicallayer::Ieee80211ModeSet *modeSet) override; + virtual void completeModeSetChange(const physicallayer::Ieee80211ModeSet *modeSet) override; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned index e32eaba59fc..edac5618a58 100644 --- a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned @@ -15,6 +15,7 @@ import inet.linklayer.ieee80211.mgmt.IIeee80211Mgmt; import inet.linklayer.ieee80211.mib.Ieee80211Mib; import inet.networklayer.common.NetworkInterface; import inet.physicallayer.wireless.common.contract.packetlevel.IRadio; +import inet.physicallayer.wireless.ieee80211.contract.packetlevel.IIeee80211ModeSetCoordinator; // // Implements an IEEE 802.11 network interface. It implements @@ -58,9 +59,11 @@ import inet.physicallayer.wireless.common.contract.packetlevel.IRadio; // Despite its appearance, Ieee80211Interface is not a plain compound module. // It has an underlying custom C++ class that inherits from `cModule`. // -module Ieee80211Interface extends NetworkInterface like IWirelessInterface +module Ieee80211Interface extends NetworkInterface like IWirelessInterface, IIeee80211ModeSetCoordinator { parameters: + @class(Ieee80211Interface); + @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); string interfaceTableModule; string energySourceModule = default(""); string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); @@ -73,6 +76,7 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface **.bitrate = this.bitrate; mac.modeSet = default(this.opMode); mac.*.rateSelection.dataFrameBitrate = default(this.bitrate); + radio.modeSetCoordinatorModule = default(absPath(".")); *.macModule = default(absPath(".mac")); *.mibModule = default(absPath(".mib")); *.interfaceTableModule = default(absPath(this.interfaceTableModule)); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index 8325bb27836..9943ba2b3e5 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -11,6 +11,7 @@ #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" #include "inet/common/ProtocolTag_m.h" #include "inet/common/packet/Message.h" #include "inet/common/packet/Packet.h" @@ -28,9 +29,8 @@ #include "inet/networklayer/contract/IInterfaceTable.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" -#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" -#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet { namespace ieee80211 { @@ -59,6 +59,8 @@ void Ieee80211Mac::initialize(int stage) fcsMode = parseFcsMode(par("fcsMode")); mib.reference(this, "mibModule", true); mib->qos = par("qosStation"); + radio = check_and_cast(gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + check_and_cast(getContainingNicModule(this))->registerModeSetConsumer(this, physicallayer::IIeee80211ModeSetCoordinator::MAC_STATE); } else if (stage == INITSTAGE_LINK_LAYER) { cModule *llcModule = gate("upperLayerOut")->getNextGate()->getOwnerModule(); @@ -68,13 +70,10 @@ 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")); tx = check_and_cast(getSubmodule("tx")); - updateLocalHtCapabilities(); - emit(modesetChangedSignal, modeSet); if (isUp()) initializeRadioMode(); rx = check_and_cast(getSubmodule("rx")); @@ -93,20 +92,18 @@ void Ieee80211Mac::updateLocalHtCapabilities() std::set operationalChannelWidths; const IIeee80211Band *operationBand = nullptr; 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"); - if (getSimulation()->getContextType() != CTX_INITIALIZE && - mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) { - const auto *channel = transmitter->getChannel(); + const auto *phy = dynamic_cast(radio.get()); + if (phy == nullptr) + throw cRuntimeError("HT operation requires the IIeee80211Radio capability contract"); + if (mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) { + mib->requirePrimaryChannel(); + const auto *channel = phy->getChannel(); if (channel == nullptr || channel->getBand() == nullptr) throw cRuntimeError("HT operation requires a configured channel and band"); operationBand = channel->getBand(); } for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) - if (transmitter->isHtChannelWidthSupported(channelWidth) && - receiver->isHtChannelWidthSupported(channelWidth)) + if (phy->isHtChannelWidthSupported(channelWidth)) operationalChannelWidths.insert(channelWidth); } mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit, operationBand); @@ -364,8 +361,7 @@ 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)); + // Mode-set application uses the coordinator contract, not notifications. } void Ieee80211Mac::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned index 51f4198660a..9e3a7d5c54f 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned @@ -92,7 +92,6 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac @display("i=block/layer"); @class(Ieee80211Mac); @signal[linkBroken](type=inet::Packet); // TODO this signal is only present for the statistic to pass the signal check, to be removed - @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); @signal[frameTransmissionOutcome](type=inet::Packet); @statistic[packetSentToUpper](title="packets sent to upper layer"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToLower](title="packets sent to lower layer"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index b295f45c983..93c0203593c 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -22,7 +22,7 @@ Define_Module(Dcaf); void Dcaf::initialize(int stage) { if (stage == INITSTAGE_LOCAL) { - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); + ModeSetListener::initialize(stage); } else if (stage == INITSTAGE_LINK_LAYER) { // TODO calculateTimingParameters() @@ -65,7 +65,8 @@ void Dcaf::calculateTimingParameters() cwMin = modeSet->getCwMin(); if (cwMax == -1) cwMax = modeSet->getCwMax(); - cw = cwMin; + // Model reconfiguration preserves retry backoff within the new bounds. + cw = std::min(cwMax, std::max(cwMin, cw)); EV_DEBUG << "Contention window parameters are initialized: cw = " << cw << ", cwMin = " << cwMin << ", cwMax = " << cwMax << std::endl; } @@ -125,8 +126,7 @@ 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)); + // Mode-set application uses the coordinator contract, not notifications. } void Dcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index 6a2cdcba2f2..f6956cfe826 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -29,7 +29,7 @@ Edcaf::~Edcaf() void Edcaf::initialize(int stage) { if (stage == INITSTAGE_LOCAL) { - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); + ModeSetListener::initialize(stage); ac = getAccessCategory(par("accessCategory")); contention = check_and_cast(getSubmodule("contention")); collisionController = check_and_cast(getModuleByPath(par("collisionControllerModule"))); @@ -74,7 +74,8 @@ void Edcaf::calculateTimingParameters() cwMin = getCwMin(ac, modeSet->getCwMin()); if (cwMax == -1) cwMax = getCwMax(ac, modeSet->getCwMax(), modeSet->getCwMin()); - cw = cwMin; + // Model reconfiguration preserves retry backoff within the new bounds. + cw = std::min(cwMax, std::max(cwMin, cw)); EV_DEBUG << "Contention window parameters are initialized: cw = " << cw << ", cwMin = " << cwMin << ", cwMax = " << cwMax << std::endl; } @@ -189,8 +190,7 @@ 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)); + // Mode-set application uses the coordinator contract, not notifications. } void Edcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc index e1a6ec01da2..79803680ef3 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/common/ModuleAccess.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" #include "inet/common/Simsignals.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -17,14 +18,13 @@ namespace ieee80211 { void ModeSetListener::initialize(int stage) { if (stage == INITSTAGE_LOCAL) - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); + check_and_cast(getContainingNicModule(this))->registerModeSetConsumer(this, physicallayer::IIeee80211ModeSetCoordinator::DERIVED_STATE); } 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)); + // Mode-set application uses the coordinator contract, not notifications. } void ModeSetListener::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index e91291f31ed..5dbc3d3fe09 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -64,8 +64,7 @@ void RateControlBase::receiveSignal(cComponent *source, simsignal_t signalID, cO { Enter_Method("%s", cComponent::getSignalName(signalID)); - if (signalID == modesetChangedSignal && obj != modeSet) - applyModeSet(check_and_cast(obj)); + // Mode-set application uses the coordinator contract, not notifications. } void RateControlBase::applyModeSet(const Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 37af3da9f9d..977e0411eb8 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -249,8 +249,7 @@ const IIeee80211Mode *QosRateSelection::computeMode(Packet *packet, const Ptr(obj)); + // Mode-set application uses the coordinator contract, not notifications. } void QosRateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 67101aceaab..ae687daf962 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/common/ModuleAccess.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" #include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" @@ -28,7 +29,7 @@ void RateSelection::initialize(int stage) { if (stage == INITSTAGE_LOCAL) { mib.reference(this, "mibModule", true); - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); + check_and_cast(getContainingNicModule(this))->registerModeSetConsumer(this, physicallayer::IIeee80211ModeSetCoordinator::DERIVED_STATE); } else if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); @@ -197,8 +198,7 @@ const IIeee80211Mode *RateSelection::computeMode(Packet *packet, const Ptr(obj)); + // Mode-set application uses the coordinator contract, not notifications. } void RateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc index 329f439ccc0..e119af0f88d 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc @@ -17,7 +17,7 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" -#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" namespace inet { @@ -69,13 +69,10 @@ const physicallayer::IIeee80211Band *Ieee80211MgmtApBase::getHtOperationBand() c { if (radio == nullptr) throw cRuntimeError("HT Operation channel conversion requires a configured radioModule"); - const auto *radioContract = dynamic_cast(radio); - if (radioContract == nullptr) - throw cRuntimeError("HT Operation channel conversion requires radioModule to reference a radio, got %s", radio->getClassName()); - const auto *transmitter = dynamic_cast(radioContract->getTransmitter()); - if (transmitter == nullptr) - throw cRuntimeError("HT Operation channel conversion requires radioModule's transmitter to provide an IEEE 802.11 channel"); - const auto *channel = transmitter->getChannel(); + const auto *phy = dynamic_cast(radio); + if (phy == nullptr) + throw cRuntimeError("HT Operation channel conversion requires the IIeee80211Radio capability contract"); + const auto *channel = phy->getChannel(); if (channel == nullptr || channel->getBand() == nullptr) throw cRuntimeError("HT Operation channel conversion requires radioModule's IEEE 802.11 transmitter to have a configured channel and band"); return channel->getBand(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 28845777bc1..8ed143121e6 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -9,6 +9,7 @@ #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" #include "inet/common/ProtocolTag_m.h" #include "inet/common/lifecycle/LifecycleOperation.h" #include "inet/common/lifecycle/ModuleOperations.h" @@ -34,7 +35,7 @@ void Ieee80211MgmtBase::initialize(int stage) myIface = getContainingNicModule(this); numMgmtFramesReceived = 0; numMgmtFramesDropped = 0; - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); + check_and_cast(getContainingNicModule(this))->registerModeSetConsumer(this, physicallayer::IIeee80211ModeSetCoordinator::DERIVED_STATE); WATCH(numMgmtFramesReceived); WATCH(numMgmtFramesDropped); } @@ -43,8 +44,7 @@ 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)); + // Mode-set application uses the coordinator contract, not notifications. } void Ieee80211MgmtBase::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h new file mode 100644 index 00000000000..2161081afd5 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h @@ -0,0 +1,40 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IIEEE80211MODESETCOORDINATOR_H +#define __INET_IIEEE80211MODESETCOORDINATOR_H + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +class Ieee80211ModeSet; + +/** + * Same-interface mode-set transaction owner. Registration is explicit and unrelated + * to signal subscriptions. Consumers are modules implementing IIeee80211ModeSetListener. + * MAC_STATE precedes DERIVED_STATE; consumers within DERIVED_STATE are independent. + * Begin validates membership before PHY mutation; complete applies consumers and + * publishes the completed fact. Any failure after begin is fatal, without rollback. + */ +class INET_API IIeee80211ModeSetCoordinator +{ + public: + enum Phase { MAC_STATE, DERIVED_STATE }; + virtual ~IIeee80211ModeSetCoordinator() = default; + // Register during initialization; repeated registration in the same phase is idempotent. + virtual void registerModeSetConsumer(cModule *consumer, Phase phase) = 0; + // Explicitly detach before deleting a consumer. Membership cannot change during a transaction. + virtual void unregisterModeSetConsumer(cModule *consumer) = 0; + virtual void beginModeSetChange(const Ieee80211ModeSet *modeSet) = 0; + virtual void completeModeSetChange(const Ieee80211ModeSet *modeSet) = 0; +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.ned b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.ned new file mode 100644 index 00000000000..2b0a6e2e26b --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.ned @@ -0,0 +1,14 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +package inet.physicallayer.wireless.ieee80211.contract.packetlevel; + +// Coordinates registered MAC/PHY state consumers before publishing a mode-set fact. +moduleinterface IIeee80211ModeSetCoordinator +{ + parameters: + @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); +} diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h index af4fcf4f988..420b58cea0c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h @@ -15,8 +15,9 @@ namespace physicallayer { class Ieee80211ModeSet; /** - * Behavioral consumer of a radio's mode-set changes. The radio applies all - * consumers before publishing modesetChanged, so observers see consistent state. + * Explicitly registered consumer of an interface's mode-set changes. The + * coordinator applies consumers before publishing modesetChanged. Implementing + * this contract or subscribing to that signal does not register a consumer. * Applying a change must not notify observers. Failures are fatal simulation * errors; partially applied changes are not rolled back. */ diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h new file mode 100644 index 00000000000..f938d841ce3 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h @@ -0,0 +1,32 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IIEEE80211RADIO_H +#define __INET_IIEEE80211RADIO_H + +#include "inet/common/INETDefs.h" +#include "inet/common/Units.h" + +namespace inet { +namespace physicallayer { + +class Ieee80211Channel; + +/** Read-only IEEE 802.11 PHY capabilities, in addition to the IRadio role. */ +class INET_API IIeee80211Radio +{ + public: + virtual ~IIeee80211Radio() = default; + // True only when both the operational transmitter and receiver support this width. + virtual bool isHtChannelWidthSupported(units::values::Hz channelWidth) const = 0; + // Borrowed channel, or nullptr when no channel is configured. + virtual const Ieee80211Channel *getChannel() const = 0; +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.ned b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.ned new file mode 100644 index 00000000000..42fe7ef481e --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.ned @@ -0,0 +1,14 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +package inet.physicallayer.wireless.ieee80211.contract.packetlevel; + +import inet.physicallayer.wireless.common.contract.packetlevel.IRadio; + +// IEEE 802.11 radio with operational HT width and channel queries. +moduleinterface IIeee80211Radio extends IRadio +{ +} diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index f61b952094b..906910a00e2 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -674,8 +674,6 @@ const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode 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(); @@ -685,7 +683,7 @@ const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode // Absence is part of the source tuple, not findMode()'s wildcard. const bool guardIntervalMatches = (sourceGuardInterval < SIMTIME_ZERO && candidateGuardInterval < SIMTIME_ZERO) || (sourceGuardInterval >= SIMTIME_ZERO && candidateGuardInterval == sourceGuardInterval); - if (minBitrate <= candidateDataMode->getNetBitrate() && candidateDataMode->getNetBitrate() <= maxBitrate && + if (candidateDataMode->getNetBitrate() == sourceBitrate && bandwidthMatches && candidateDataMode->getNumberOfSpatialStreams() == sourceDataMode->getNumberOfSpatialStreams() && guardIntervalMatches) return entry.mode; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 12b3cc1576f..2752f9c0ad4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -47,6 +47,9 @@ void Ieee80211Radio::initialize(int stage) FlatRadioBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { + modeSetCoordinator.reference(this, "modeSetCoordinatorModule", false); + if (modeSetCoordinator != nullptr && dynamic_cast(modeSetCoordinator.get()) != getParentModule()) + throw cRuntimeError("The mode-set coordinator must be the containing interface"); const char *fcsModeString = par("fcsMode"); fcsMode = parseFcsMode(fcsModeString, true); } @@ -109,33 +112,35 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 throw cRuntimeError("Invalid mode"); auto transmitter = const_cast(check_and_cast(this->transmitter)); auto receiver = const_cast(check_and_cast(this->receiver)); - // Apply each behavioral consumer once before notifying observers. - std::vector participants; - 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"); + if (modeSetCoordinator != nullptr) + modeSetCoordinator->beginModeSetChange(modeSet); changingModeSet = true; if (explicitMode) transmitter->setModeSetAndMode(modeSet, mode); else transmitter->setModeSet(modeSet); receiver->setModeSet(modeSet); - for (auto participant : participants) - participant->applyModeSet(modeSet); receptionTimer = nullptr; - if (modeSet != nullptr) + if (modeSetCoordinator != nullptr) + modeSetCoordinator->completeModeSetChange(modeSet); + else if (modeSet != nullptr) emit(modesetChangedSignal, const_cast(modeSet)); emit(listeningChangedSignal, 0); changingModeSet = false; EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; } +const Ieee80211Channel *Ieee80211Radio::getChannel() const +{ + return check_and_cast(transmitter)->getChannel(); +} + +bool Ieee80211Radio::isHtChannelWidthSupported(Hz channelWidth) const +{ + return check_and_cast(transmitter)->isHtChannelWidthSupported(channelWidth) && + check_and_cast(receiver)->isHtChannelWidthSupported(channelWidth); +} + void Ieee80211Radio::setMode(const IIeee80211Mode *mode) { Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 0fc525cbfeb..586d1144f1c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -9,6 +9,9 @@ #define __INET_IEEE80211RADIO_H #include "inet/physicallayer/wireless/common/base/packetlevel/FlatRadioBase.h" +#include "inet/common/ModuleRefByPar.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" @@ -18,7 +21,7 @@ namespace inet { namespace physicallayer { -class INET_API Ieee80211Radio : public FlatRadioBase +class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio { public: /** @@ -30,6 +33,7 @@ class INET_API Ieee80211Radio : public FlatRadioBase static const Ptr peekIeee80211PhyHeaderAtFront(const Packet *packet, b length = b(-1), int flags = 0); protected: + ModuleRefByPar modeSetCoordinator; bool changingModeSet = false; FcsMode fcsMode = FCS_MODE_UNDEFINED; @@ -52,6 +56,8 @@ class INET_API Ieee80211Radio : public FlatRadioBase // Update behavioral consumers before publishing the new mode set. // Failures are fatal simulation errors; these setters do not roll back. // Behavioral consumers implement IIeee80211ModeSetListener. + virtual const Ieee80211Channel *getChannel() const override; + virtual bool isHtChannelWidthSupported(Hz channelWidth) const override; 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/Ieee80211Radio.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned index c32061114d3..c85451199c0 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned @@ -8,6 +8,7 @@ package inet.physicallayer.wireless.ieee80211.packetlevel; import inet.physicallayer.wireless.common.base.packetlevel.FlatRadioBase; +import inet.physicallayer.wireless.ieee80211.contract.packetlevel.IIeee80211Radio; // // This radio model is part of the IEEE 802.11 physical layer model. It supports @@ -22,9 +23,11 @@ import inet.physicallayer.wireless.common.base.packetlevel.FlatRadioBase; // @see ~Ieee80211ScalarRadio, ~Ieee80211DimensionalRadio. // //# TODO check this Table 18-14—Receiver performance requirements -module Ieee80211Radio extends FlatRadioBase +module Ieee80211Radio extends FlatRadioBase like IIeee80211Radio { parameters: + string modeSetCoordinatorModule = default(""); // Optional interface transaction owner; empty for standalone PHY use + @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); // Standalone radio only string opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-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) diff --git a/tests/module/Ieee80211ContentionModeSet_1.test b/tests/module/Ieee80211ContentionModeSet_1.test index af38b0fa192..7a80b0c3140 100644 --- a/tests/module/Ieee80211ContentionModeSet_1.test +++ b/tests/module/Ieee80211ContentionModeSet_1.test @@ -4,7 +4,10 @@ whole remaining slots without another random draw. Cover IFS, backoff, DEFER, EIFS and unchanged timing, and observe the actual channel grant time. %file: ContentionModeSet.cc +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" #include "inet/linklayer/ieee80211/mac/contention/Contention.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" #include "inet/linklayer/ieee80211/mac/contract/IChannelAccess.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" @@ -12,6 +15,15 @@ using namespace inet; using namespace inet::ieee80211; using namespace inet::physicallayer; +class BoundedCwMode : public Ieee80211OfdmMode +{ + int upper; + public: + BoundedCwMode(int upper) : Ieee80211OfdmMode(*check_and_cast(Ieee80211ModeSet::getModeSet("a")->getMode(Mbps(6)))), upper(upper) {} + virtual int getLegacyCwMin() const override { return 127; } + virtual int getLegacyCwMax() const override { return upper; } +}; + class TimingProbeContention : public Contention { public: @@ -28,17 +40,42 @@ class ContentionModeSetTest : public cSimpleModule, public IChannelAccess::ICall { TimingProbeContention *contention = nullptr; IChannelAccess *access = nullptr; + cModule *accessModule = nullptr; + + int currentCw() { return par("qos").boolValue() ? check_and_cast(accessModule)->getCw() : check_and_cast(accessModule)->getCw(); } + void incrementCw() + { + if (par("qos").boolValue()) + check_and_cast(accessModule)->incrementCw(); + else + check_and_cast(accessModule)->incrementCw(); + } Ieee80211Radio *radio = nullptr; simtime_t expectedGrant; simtime_t previousGrant; int initialSlots = 0; int draws = 0; bool switched = false; + int modeNotifications = 0; std::string phase; protected: virtual void initialize() override { scheduleAt(SimTime(1, SIMTIME_US), new cMessage("start")); } virtual void receiveSignal(cComponent *, simsignal_t, intval_t, cObject *) override { draws++; } + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *value, cObject *) override + { + Enter_Method_Silent(); + ASSERT(source == radio->getParentModule()); + auto target = check_and_cast(value); + ASSERT(contention->slot() == target->getSlotTime()); + ASSERT(draws == 1); + if (contention->state() == Contention::IFS_AND_BACKOFF) { + auto interval = phase == "eifs" ? contention->extendedInterframe() : contention->interframe(); + auto expected = phase == "unchanged" ? previousGrant : simTime() + interval + contention->slots() * target->getSlotTime(); + ASSERT(contention->grantTime() == expected); + } + modeNotifications++; + } virtual void handleMessage(cMessage *message) override { delete message; @@ -46,11 +83,15 @@ class ContentionModeSetTest : public cSimpleModule, public IChannelAccess::ICall phase = par("phase").stdstringValue(); auto nic = getParentModule()->getSubmodule("ap")->getSubmodule("wlan", 0); radio = check_and_cast(nic->getSubmodule("radio")); - auto accessModule = nic->getSubmodule("mac")->getModuleByPath(par("qos").boolValue() ? ".hcf.edca.edcaf[1]" : ".dcf.channelAccess"); + nic->subscribe(modesetChangedSignal, this); + accessModule = nic->getSubmodule("mac")->getModuleByPath(par("qos").boolValue() ? ".hcf.edca.edcaf[1]" : ".dcf.channelAccess"); access = check_and_cast(accessModule); contention = check_and_cast(accessModule->getSubmodule("contention")); contention->subscribe(IContention::backoffPeriodGeneratedSignal, this); contention->mediumStateChanged(phase != "defer"); + ASSERT(currentCw() == 31); + incrementCw(); + ASSERT(currentCw() == 63); access->requestChannel(this); initialSlots = contention->slots(); ASSERT(initialSlots >= 2); @@ -68,6 +109,8 @@ class ContentionModeSetTest : public cSimpleModule, public IChannelAccess::ICall ASSERT(contention->state() == (phase == "defer" ? Contention::DEFER : Contention::IFS_AND_BACKOFF)); const auto *target = Ieee80211ModeSet::getModeSet(phase == "unchanged" ? "g(mixed)" : "g(erp)"); radio->setModeSetAndMode(target, target->getMode(Mbps(24))); + ASSERT(currentCw() == 63); + ASSERT(modeNotifications == 1); ASSERT(contention->slot() == target->getSlotTime()); ASSERT(contention->interframe() == target->getSifsTime() + (par("qos").boolValue() ? 3 : 2) * target->getSlotTime()); ASSERT(contention->slots() == initialSlots - (phase == "backoff" ? 1 : 0)); @@ -89,6 +132,18 @@ class ContentionModeSetTest : public cSimpleModule, public IChannelAccess::ICall ASSERT(draws == 1); access->releaseChannel(this); contention->unsubscribe(IContention::backoffPeriodGeneratedSignal, this); + radio->getParentModule()->unsubscribe(modesetChangedSignal, this); + // Exercise both clamp boundaries after retry-window growth. + auto participant = check_and_cast(accessModule); + static BoundedCwMode lowerMode(1023), upperMode(127); + static Ieee80211ModeSet lowerSet("lower bound", {{true, &lowerMode, true}}, &lowerMode, Ieee80211ModeSet::getModeSet("a")->getPhyType()); + static Ieee80211ModeSet upperSet("upper bound", {{true, &upperMode, true}}, &upperMode, Ieee80211ModeSet::getModeSet("a")->getPhyType()); + participant->applyModeSet(&lowerSet); + ASSERT(currentCw() == 127); + incrementCw(); + ASSERT(currentCw() == 255); + participant->applyModeSet(&upperSet); + ASSERT(currentCw() == 127); std::cout << "Verified contention phase=" << phase << " qos=" << par("qos").boolValue() << "\n"; endSimulation(); } @@ -142,8 +197,8 @@ record-scalar-results = false **.mgmt.beaconInterval = 1s **.contention.typename = "TimingProbeContention" **.contention.backoffOptimization = false -**.cwMin = 31 -**.cwMax = 1023 +**.cwMin = -1 +**.cwMax = -1 %file: check_results.py from pathlib import Path diff --git a/tests/module/Ieee80211MgmtApUnavailableChannel_1.test b/tests/module/Ieee80211MgmtApUnavailableChannel_1.test index 5db13b43fb1..e0782ca279f 100644 --- a/tests/module/Ieee80211MgmtApUnavailableChannel_1.test +++ b/tests/module/Ieee80211MgmtApUnavailableChannel_1.test @@ -1,8 +1,8 @@ %description: An HT-capable AP must not serialize a management frame before its radio has published a valid primary channel. A radio channel of -1 suppresses the -initial channel signal, so the first beacon deterministically reports the -missing MIB-owned channel instead of serializing an invalid value. +initial channel signal, so initial mode-set application reports the missing +MIB-owned channel before a notification or Beacon can expose invalid state. %file: test.ned diff --git a/tests/module/Ieee80211ModeSetFailure_1.test b/tests/module/Ieee80211ModeSetFailure_1.test index ecc874efee4..ba5be7dc502 100644 --- a/tests/module/Ieee80211ModeSetFailure_1.test +++ b/tests/module/Ieee80211ModeSetFailure_1.test @@ -20,8 +20,10 @@ class ModeSetFailure : public cSimpleModule, public cListener virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { Enter_Method_Silent(); + if (par("failure").stdstringValue() == "detach") + check_and_cast(source)->unregisterModeSetConsumer(check_and_cast(source)->getSubmodule("mac")); if (par("failure").stdstringValue() == "reentrant") - check_and_cast(source)->setModeSet(check_and_cast(value)); + check_and_cast(check_and_cast(source)->getSubmodule("radio"))->setModeSet(check_and_cast(value)); throw cRuntimeError("test observer rejects mode set"); } virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override @@ -37,8 +39,8 @@ class ModeSetFailure : public cSimpleModule, public cListener auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); radio->setMode(ht->getMode(Mbps(24))); auto failure = par("failure").stdstringValue(); - if (failure == "modeset" || failure == "reentrant") - radio->subscribe(modesetChangedSignal, this); + if (failure == "modeset" || failure == "reentrant" || failure == "detach") + radio->getParentModule()->subscribe(modesetChangedSignal, this); else if (failure == "listening") radio->subscribe(IRadio::listeningChangedSignal, this); std::cout << "Attempting " << failure << " explicit=" << par("explicitMode").boolValue() << "\n"; @@ -84,7 +86,7 @@ cmdenv-express-mode = false cmdenv-stop-batch-on-error = false record-vector-results = false record-scalar-results = false -*.test.failure = ${failure="dcf","hcf","modeset","listening","reentrant","band"} +*.test.failure = ${failure="dcf","hcf","modeset","listening","reentrant","band","detach"} *.test.explicitMode = ${false,true} **.mobility.initFromDisplayString = false **.mobility.initialX = 10m @@ -108,16 +110,17 @@ record-scalar-results = false %file: check_errors.py from pathlib import Path text = Path("test.out").read_text() + Path("test.err").read_text() -for failure in ("dcf", "hcf", "modeset", "listening", "reentrant", "band"): +for failure in ("dcf", "hcf", "modeset", "listening", "reentrant", "band", "detach"): for explicit in (0, 1): assert text.count(f"Attempting {failure} explicit={explicit}") == 1 errors = [line for line in text.splitlines() if " Error:" in line] -assert len(errors) == 12, errors +assert len(errors) == 14, errors assert sum("Unknown mode for bitrate" in line for line in errors) == 4, errors assert sum("test observer rejects mode set" in line for line in errors) == 2, errors assert sum("test observer rejects listening change" in line for line in errors) == 2, errors assert sum("Reentrant radio mode-set change" in line for line in errors) == 2, errors assert sum("Invalid primary channel" in line for line in errors) == 2, errors +assert sum("Cannot detach a mode-set consumer during a transition" in line for line in errors) == 2, errors assert "UNEXPECTED successful" not in text %postrun-command: python3 check_errors.py diff --git a/tests/module/Ieee80211ModeSetRegistration_1.test b/tests/module/Ieee80211ModeSetRegistration_1.test new file mode 100644 index 00000000000..9b278e18f1f --- /dev/null +++ b/tests/module/Ieee80211ModeSetRegistration_1.test @@ -0,0 +1,232 @@ +%description: +Explicit mode-set registration is independent of observer subscriptions and registration +order. The interface publishes once after initialization and after each runtime change, +with MAC state already applied. Cover interface isolation, duplicate registration, +unregistration/deletion, and rejection of late and cross-interface registration. + +%file: Registration.cc +#include "inet/physicallayer/wireless/generic/GenericRadio.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/common/ModuleAccess.h" +#include "inet/networklayer/common/NetworkInterface.h" +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.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; + +// A capability provider with generic PHY roles proves that MAC/management use +// the radio contract, not concrete IEEE transmitter/receiver implementations. +class CapabilityRadio : public GenericRadio, public IIeee80211Radio +{ + Ieee80211Channel channel{&Ieee80211CompliantBands::band2_4GHz, 10}; + public: + virtual bool isHtChannelWidthSupported(Hz width) const override { return width == MHz(20) || width == MHz(40); } + virtual const Ieee80211Channel *getChannel() const override { return &channel; } + protected: + virtual void initialize(int stage) override + { + GenericRadio::initialize(stage); + if (stage == INITSTAGE_PHYSICAL_LAYER) + emit(cComponent::registerSignal("radioChannelChanged"), 10L); + } +}; +Define_Module(CapabilityRadio); + +class RegisteredConsumer : public cSimpleModule, public IIeee80211ModeSetListener +{ + const Ieee80211ModeSet *modeSet = nullptr; + public: + int applications = 0; + virtual const Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual void applyModeSet(const Ieee80211ModeSet *value) override + { + Enter_Method_Silent(); + auto nic = getContainingNicModule(this); + ASSERT(check_and_cast(nic->getSubmodule("mac"))->getModeSet() == value); + ASSERT(check_and_cast(nic->getSubmodule("mib"))->isHtOperationSupported() == value->isHtOperationSupported()); + modeSet = value; + applications++; + } + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + virtual void initialize(int stage) override + { + bool late = (getIndex() == 0) == par("reverse").boolValue(); + if (stage == (late ? INITSTAGE_PHYSICAL_LAYER : INITSTAGE_LOCAL)) { + auto coordinator = check_and_cast(getContainingNicModule(this)); + bool rejected = false; + try { coordinator->registerModeSetConsumer(this, static_cast(99)); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + coordinator->registerModeSetConsumer(this, IIeee80211ModeSetCoordinator::DERIVED_STATE); + coordinator->registerModeSetConsumer(this, IIeee80211ModeSetCoordinator::DERIVED_STATE); + } + } +}; +Define_Module(RegisteredConsumer); + +// Merely implementing the consumer interface and subscribing must never enlist it. +class UnregisteredObserver : public cListener, public IIeee80211ModeSetListener +{ + public: + int notifications = 0; + virtual const Ieee80211ModeSet *getModeSet() const override { return nullptr; } + virtual void applyModeSet(const Ieee80211ModeSet *) override { throw cRuntimeError("Observer enlisted as participant"); } + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { notifications++; } +}; + +class RegistrationTest : public cSimpleModule, public cListener +{ + UnregisteredObserver observer; + int notifications = 0; + RegisteredConsumer *probe(cModule *nic, int index) { return check_and_cast(nic->getSubmodule("probe", index)); } + cModule *nic(int index) { return getParentModule()->getSubmodule("ap", index)->getSubmodule("wlan", 0); } + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) { + getParentModule()->subscribe(modesetChangedSignal, this); + getParentModule()->subscribe(modesetChangedSignal, &observer); + } + else if (stage == INITSTAGE_LAST) + scheduleAfter(SimTime(1, SIMTIME_US), new cMessage("change")); + } + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *value, cObject *) override + { + Enter_Method_Silent(); + ASSERT(source == nic(0) || source == nic(1)); + auto module = check_and_cast(source); + auto target = check_and_cast(value); + ASSERT(probe(module, 0)->getModeSet() == target); + ASSERT(probe(module, 1)->getModeSet() == target); + if (module == nic(1)) { + auto mib = check_and_cast(module->getSubmodule("mib")); + ASSERT(mib->localHtCapabilities.supportedChannelWidths.count(MHz(40)) == 1); + ASSERT(mib->requirePrimaryChannel() == 10); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(20)); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 0); + } + notifications++; + } + virtual void handleMessage(cMessage *message) override + { + delete message; + ASSERT(notifications == 2 && observer.notifications == 2); + auto first = nic(0); + auto second = nic(1); + auto radio = check_and_cast(first->getSubmodule("radio")); + auto coordinator = check_and_cast(first); + auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); + auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + ASSERT(notifications == 3 && observer.notifications == 3); + ASSERT(probe(first, 0)->applications == 2 && probe(first, 1)->applications == 2); + ASSERT(probe(second, 0)->applications == 1 && probe(second, 1)->applications == 1); + getParentModule()->unsubscribe(modesetChangedSignal, this); + getParentModule()->unsubscribe(modesetChangedSignal, &observer); + radio->setModeSet(ht); + ASSERT(probe(first, 0)->applications == 3 && probe(first, 1)->applications == 3); + ASSERT(notifications == 3 && observer.notifications == 3); + bool rejected = false; + try { coordinator->registerModeSetConsumer(probe(first, 0), IIeee80211ModeSetCoordinator::DERIVED_STATE); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + rejected = false; + try { coordinator->unregisterModeSetConsumer(probe(second, 0)); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + coordinator->unregisterModeSetConsumer(probe(first, 1)); + probe(first, 1)->deleteModule(); + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + ASSERT(probe(first, 0)->applications == 4); + ASSERT(probe(second, 0)->applications == 1); + if (par("deleteRegistered").boolValue()) + probe(first, 0)->deleteModule(); // Deliberately omit unregistering. + else + coordinator->unregisterModeSetConsumer(first->getSubmodule("mgmt")); + rejected = false; + try { radio->setModeSetAndMode(ht, ht->getMode(Mbps(24))); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + // Missing membership is rejected before the PHY is mutated. + ASSERT(check_and_cast(radio->getTransmitter())->getModeSet() == legacy); + std::cout << "Explicit registration, publication, observer neutrality, isolation and teardown verified.\n"; + endSimulation(); + } +}; +Define_Module(RegistrationTest); + +%file: test.ned +import inet.node.wireless.AccessPoint; +import inet.linklayer.ieee80211.Ieee80211Interface; +import inet.physicallayer.wireless.common.medium.UnitDiskRadioMedium; +import inet.physicallayer.wireless.generic.GenericUnitDiskRadio; +import inet.physicallayer.wireless.ieee80211.contract.packetlevel.IIeee80211Radio; +module CapabilityRadio extends GenericUnitDiskRadio like IIeee80211Radio +{ + parameters: + @class(::CapabilityRadio); +} +simple RegisteredConsumer +{ + parameters: + bool reverse; + @class(::RegisteredConsumer); +} +module RegisteredInterface extends Ieee80211Interface +{ + submodules: + probe[2]: RegisteredConsumer; +} +simple RegistrationTest +{ + parameters: + bool deleteRegistered; + @class(::RegistrationTest); +} +network RegistrationNetwork +{ + submodules: + radioMedium: UnitDiskRadioMedium; + test: RegistrationTest; + ap[2]: AccessPoint; +} + +%inifile: omnetpp.ini +[General] +network = RegistrationNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.ap[*].wlan[*].typename = "RegisteredInterface" +**.probe[*].reverse = ${false,true} +*.test.deleteRegistered = ${false,true} +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +*.ap[0].wlan[*].radio.typename = "Ieee80211UnitDiskRadio" +*.ap[1].wlan[*].radio.typename = "CapabilityRadio" +*.ap[1].wlan[*].mib.htSecondaryChannelOffset = 1 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 24Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].radio.transmitter.power = 100mW +**.radio.transmitter.analogModel.communicationRange = 100m +**.radio.transmitter.analogModel.interferenceRange = 100m +**.radio.transmitter.analogModel.detectionRange = 100m +**.mgmt.ssid = "registration" +**.mgmt.beaconInterval = 1s + +%contains: stdout +Explicit registration, publication, observer neutrality, isolation and teardown verified. diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test index bc0487158e1..26a64bc573a 100644 --- a/tests/module/Ieee80211ModeSetTransition_1.test +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -6,6 +6,7 @@ peer state and adaptive rates before observers run, and notify the radio medium. %file: Ieee80211ModeSetTransition.cc #include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" #include "inet/physicallayer/wireless/common/medium/RadioMedium.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" @@ -97,13 +98,15 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener CachingModeSetObserver cachingObserver; MacAddress peer = MacAddress("02:00:00:00:01:ff"); - std::vector participants(cModule *radio) + std::vector participants(cModule *module) { 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); + if (auto participant = dynamic_cast(module)) + result.push_back(participant); + for (cModule::SubmoduleIterator it(module); !it.end(); ++it) { + auto nested = participants(*it); + result.insert(result.end(), nested.begin(), nested.end()); + } return result; } @@ -112,7 +115,7 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener 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)) + for (auto participant : participants(nic)) ASSERT(participant->getModeSet() == expected); auto mib = check_and_cast(nic->getSubmodule("mib")); ASSERT(mib->isHtOperationSupported() == expected->isHtOperationSupported()); @@ -136,6 +139,9 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener data->setReceiverAddress(address); beacon->setReceiverAddress(address); Packet packet("selection probe"); + // A retry or protection-prepared frame may still carry an old HT mode. + auto staleMode = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")->getMode(Mbps(65), MHz(20), 1, 800e-9); + packet.addTag()->setMode(staleMode); 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")); @@ -160,15 +166,13 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener { Enter_Method_Silent(); notifications++; - auto radio = check_and_cast(source); + auto nic = check_and_cast(source); + ASSERT(nic->getSubmodule("mac") != nullptr); 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(); - for (auto participant : participants(radio)) + for (auto participant : participants(nic)) ASSERT(participant->getModeSet() == target); ASSERT(check_and_cast(nic->getSubmodule("mib"))->isHtOperationSupported() == target->isHtOperationSupported()); check_and_cast(nic->getSubmodule("mgmt"))->checkAdvertisement(); @@ -185,7 +189,7 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); radio->setMode(ht->getMode(Mbps(24))); cachingObserver.modeSet = ht; - radio->subscribe(modesetChangedSignal, &cachingObserver); + nic->subscribe(modesetChangedSignal, &cachingObserver); nic->subscribe(modesetChangedSignal, this); mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); check(nic, ht, true); @@ -211,7 +215,7 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener ASSERT(cachingObserver.modeSet == ht); ASSERT(observedModeSet == ht); nic->unsubscribe(modesetChangedSignal, this); - radio->unsubscribe(modesetChangedSignal, &cachingObserver); + nic->unsubscribe(modesetChangedSignal, &cachingObserver); std::cout << "Mode-set switching, advertisements, peer state, both selectors, and notifications verified.\n"; endSimulation(); } diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 7ab5633debd..8c5cec83e78 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -87,7 +87,7 @@ class TestRateSelectionAccessor : public ieee80211::RateSelection static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } 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); + rs->applyModeSet(ms); } }; @@ -99,7 +99,7 @@ class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } 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); + rs->applyModeSet(ms); } }; @@ -122,7 +122,7 @@ class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase public: void notifyModeSet(const Ieee80211ModeSet *modeSet) { - receiveSignal(nullptr, modesetChangedSignal, const_cast(modeSet), nullptr); + applyModeSet(modeSet); } const ieee80211::Ieee80211SupportedRatesElement& getSupportedRates() const { return supportedRates; } diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 44877fd9d68..f225dcee91c 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -38,6 +38,15 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs return nullptr; } +// Synthetic data rates isolate exact tuple matching from public lookup tolerance. +class CustomBitrateDataMode : public Ieee80211HtDataMode +{ + bps bitrate; + public: + CustomBitrateDataMode(const Ieee80211HtDataMode& base, bps bitrate) : Ieee80211HtDataMode(base), bitrate(bitrate) {} + virtual bps getNetBitrate() const override { return bitrate; } +}; + %activity: const auto *htModeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); @@ -69,6 +78,18 @@ for (int mcsIndex = 0; mcsIndex < 32; mcsIndex++) { } } } +// No preamble is needed: these synthetic modes exercise only tuple lookup. +const auto *baseData = check_and_cast( + findHtMode(htModeSet, 0, MHz(20), Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)->getDataMode()); +Ieee80211HtMode source("source", nullptr, new CustomBitrateDataMode(*baseData, Mbps(6)), Ieee80211HtMode::BAND_2_4GHZ); +Ieee80211HtMode nearby("nearby", nullptr, new CustomBitrateDataMode(*baseData, Mbps(6.04)), Ieee80211HtMode::BAND_2_4GHZ); +Ieee80211HtMode equal("equal", nullptr, new CustomBitrateDataMode(*baseData, Mbps(6)), Ieee80211HtMode::BAND_2_4GHZ); +SparseModeSet nearbySet({{true, &nearby}, {true, htModeSet->getMode(Mbps(1)), true}}, &nearby, false); +ASSERT(nearbySet.findCompatibleMode(&source) == nullptr); +ASSERT(nearbySet.findMode(Mbps(6), MHz(20), 1, 800e-9) == &nearby); +SparseModeSet equalSet({{true, &nearby}, {true, &equal}, {true, htModeSet->getMode(Mbps(1)), true}}, &equal, false); +ASSERT(equalSet.findCompatibleMode(&source) == &equal); +ASSERT(equalSet.findCompatibleMode(nullptr) == nullptr); ASSERT(htModeSet->getHtSupportedChannelWidths().size() == 2); ASSERT(htModeSet->isHtShortGuardIntervalSupported(MHz(20))); ASSERT(htModeSet->isHtShortGuardIntervalSupported(MHz(40)));