From db9d61d9a1d59078a59e4504c135fdbb2ade8f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 19 Sep 2026 01:53:48 +0200 Subject: [PATCH 01/21] plan: add: declare capability and HT/GI implementation contracts Keep the implementation contracts available before the first commit that references them. These drafts state planned work; the owning implementation and closure commits record their completed plans and verification evidence. Change: plan | behavior.add | - --- plan/done/80211htcapop-refactor-v3.md | 5 +++++ plan/done/ht-gi-devin-comment-closure.md | 27 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 plan/done/80211htcapop-refactor-v3.md create mode 100644 plan/done/ht-gi-devin-comment-closure.md diff --git a/plan/done/80211htcapop-refactor-v3.md b/plan/done/80211htcapop-refactor-v3.md new file mode 100644 index 00000000000..4e26ac707b4 --- /dev/null +++ b/plan/done/80211htcapop-refactor-v3.md @@ -0,0 +1,5 @@ +# IEEE 802.11 capability and BSS ownership plan + +Status: planned. + +Prepare interface identity before link-layer consumers initialize. Separate local capability ownership from mutable BSS state, prepare HT capabilities before management startup, and expose a read-only mode-set provider. Preserve effective configuration contracts and validate focused capability, association, discovery and initialization fixtures. Record completed evidence with implementation. diff --git a/plan/done/ht-gi-devin-comment-closure.md b/plan/done/ht-gi-devin-comment-closure.md new file mode 100644 index 00000000000..2b4fdbb9f2e --- /dev/null +++ b/plan/done/ht-gi-devin-comment-closure.md @@ -0,0 +1,27 @@ +# HT/GI Devin comment closure + +Status: planned. Verification evidence will be recorded after implementation. + +## Implementation contract + +- Invariant and owner: transmitter compatibility rejection must precede the radio/MAC transaction. + The transmitter owns exact-tuple resolution; radio coordinates updates. Failures after PHY mutation + remain fatal. A rejected preflight changes no catalog, current mode, peer cache or notification count. +- Entry/control path: both radio setters call `changeModeSet`; catalog-only changes preflight a const + transmitter query before `beginModeSetChange`. The transmitter setter reuses the same query. + Explicit changes keep their existing membership check and virtual setter dispatch. +- Affected artifacts: radio/transmitter C++ and transmitter declaration; management serializer masks; + peer-mode selector citations; `IIeee80211Mode` and existing `Ieee80211ModeBase`; transition module + and VHT management-element unit fixtures. HT/VHT duration overrides remain authoritative. +- Siblings/terminal paths: same/null catalogs retain existing transmitter semantics; explicit mode + changes and post-mutation participant failures retain their contract. Serializer read/write masks + stay symmetric. No lifecycle or packet ownership change. +- Boundaries: exact bitrate/bandwidth/NSS/GI tuple; null mode/catalog behavior unchanged. Duration + units and arithmetic unchanged. VHT subtype placement remains identical to HT, with distinct bits; + this does not add band/capability presence validation. +- Verification: debug and release library builds; filtered transition/failure/registration and VHT + association modules; HT/GI/VHT mode, management-element and peer-selection units; the earlier + capability/BSS evidence's focused units/modules/protocols and two legacy ad hoc fingerprints. + Extend the transition fixture with rejection followed by successful explicit retry, and VHT codec + coverage for request-operation rejection and independent HT/VHT presence. + From e734003855b70329b46bef7c15d4b306d9c6824b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Fri, 18 Sep 2026 20:25:07 +0200 Subject: [PATCH 02/21] common: change: prepare interface identity before link-layer users Link-layer initialization may resolve peer interfaces by their configured addresses. Declare network-interface configuration as a prerequisite so these queries see initialized identities regardless of module declaration order. The physical-layer prerequisite remains in place. This ordering supplies the shared readiness contract used by simplified wireless association before network configuration. No fingerprint or statistical baseline is changed. The selected regression contract covers MacNonQos and MacQos, run 0, in examples/adhoc/qos, plus simplified association and AP lifecycle module tests in debug mode. Plan: plan/done/80211htcapop-refactor-v3.md Change: src.common.InitStages | behavior.change | test | ieee80211-htcapop-v3 --- src/inet/common/InitStages.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/inet/common/InitStages.cc b/src/inet/common/InitStages.cc index ccd02afae27..e28cb982f6d 100644 --- a/src/inet/common/InitStages.cc +++ b/src/inet/common/InitStages.cc @@ -54,6 +54,7 @@ Define_InitStage_Dependency(QUEUEING, GATE_SCHEDULE_CONFIGURATION); Define_InitStage(LINK_LAYER); Define_InitStage_Dependency(LINK_LAYER, PHYSICAL_LAYER); +Define_InitStage_Dependency(LINK_LAYER, NETWORK_INTERFACE_CONFIGURATION); Define_InitStage(NETWORK_CONFIGURATION); Define_InitStage_Dependency(NETWORK_CONFIGURATION, LINK_LAYER); From 69f18c6f46d779b7835e712025c9862502f5982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Fri, 18 Sep 2026 20:46:35 +0200 Subject: [PATCH 03/21] ieee80211: change: separate capability and BSS state ownership Capability preparation previously depended on initialization broadcasts and mixed local PHY support with active BSS operation. Make the MAC prepare an idempotent profile through typed PHY contributors, and let management own accepted BSS transitions and simplified peer installation. Keep directional peer capabilities immutable and evaluate operation and HT eligibility separately. Publish committed MIB changes after management bookkeeping, guard state mutation, and replace modesetChanged listeners with explicit configuration providers. Prepare simplified associations before network configuration, independently of node declaration order. Keep simplified legacy STAs associated without accepting HT operation from an HT AP. Cover both declaration orders and shutdown/crash restart, while retaining BSS identity and channel and removing AP-side peer resources on stop. Migrate dependent consumers and regression fixtures together with the contracts. Document public API migration and the notification lifetime. Validation scope: debug build; 3 focused unit and 9 module cases covering capability preparation, provider wiring, association, lifecycle, beacon and channel updates; MacNonQos and MacQos run 0 fingerprints at 10s with unchanged tplx expectations. No fingerprint or statistical baseline changes. Plan: plan/done/80211htcapop-refactor-v3.md Change: src.ieee80211 | behavior.change | test expected whatsnew migration | ieee80211-htcapop-v3 --- WHATSNEW | 16 + .../design/ieee80211-model-architecture.md | 25 + doc/src/migration-guide/index.rst | 42 ++ .../done/80211htcapop-refactor-v3-evidence.md | 266 +++++++++ plan/done/80211htcapop-refactor-v3.md | 520 +++++++++++++++++- src/inet/common/Simsignals.cc | 1 - src/inet/common/Simsignals.h | 1 - src/inet/linklayer/ieee80211/__TODO | 1 - src/inet/linklayer/ieee80211/mac/Ds.cc | 12 +- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 104 +++- .../linklayer/ieee80211/mac/Ieee80211Mac.h | 6 +- .../linklayer/ieee80211/mac/Ieee80211Mac.ned | 6 +- .../OriginatorBlockAckAgreementPolicy.cc | 2 +- .../OriginatorBlockAckAgreementPolicy.h | 4 +- .../OriginatorBlockAckAgreementPolicy.ned | 1 + .../ieee80211/mac/channelaccess/Dcaf.cc | 16 +- .../ieee80211/mac/channelaccess/Dcaf.h | 6 +- .../ieee80211/mac/channelaccess/Dcaf.ned | 1 + .../ieee80211/mac/channelaccess/Edcaf.cc | 11 +- .../ieee80211/mac/channelaccess/Edcaf.h | 5 +- .../ieee80211/mac/channelaccess/Edcaf.ned | 1 + .../ieee80211/mac/common/ModeSetListener.cc | 33 -- .../ieee80211/mac/common/ModeSetListener.h | 32 -- .../ieee80211/mac/common/ModeSetModuleBase.cc | 20 + .../ieee80211/mac/common/ModeSetModuleBase.h | 26 + .../mac/contract/IIeee80211MacConfiguration.h | 27 + .../contract/IIeee80211MacConfiguration.ned | 12 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 4 +- .../ieee80211/mac/coordinationfunction/Dcf.h | 4 +- .../mac/coordinationfunction/Dcf.ned | 1 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 6 +- .../ieee80211/mac/coordinationfunction/Hcf.h | 4 +- .../mac/coordinationfunction/Hcf.ned | 1 + .../mac/framesequence/FrameSequenceContext.cc | 2 +- .../mac/framesequence/FrameSequenceContext.h | 4 +- .../mac/originator/OriginatorAckPolicy.cc | 2 +- .../mac/originator/OriginatorAckPolicy.h | 4 +- .../mac/originator/OriginatorAckPolicy.ned | 1 + .../mac/originator/OriginatorQosAckPolicy.cc | 2 +- .../mac/originator/OriginatorQosAckPolicy.h | 4 +- .../mac/originator/OriginatorQosAckPolicy.ned | 1 + .../ieee80211/mac/originator/QosRtsPolicy.cc | 2 +- .../ieee80211/mac/originator/QosRtsPolicy.h | 4 +- .../ieee80211/mac/originator/QosRtsPolicy.ned | 1 + .../ieee80211/mac/originator/RtsPolicy.cc | 2 +- .../ieee80211/mac/originator/RtsPolicy.h | 4 +- .../ieee80211/mac/originator/RtsPolicy.ned | 1 + .../ieee80211/mac/originator/TxopProcedure.cc | 2 +- .../ieee80211/mac/originator/TxopProcedure.h | 4 +- .../mac/originator/TxopProcedure.ned | 1 + .../OriginatorProtectionMechanism.cc | 2 +- .../OriginatorProtectionMechanism.h | 4 +- .../OriginatorProtectionMechanism.ned | 1 + .../SingleProtectionMechanism.cc | 2 +- .../SingleProtectionMechanism.h | 4 +- .../SingleProtectionMechanism.ned | 1 + .../mac/ratecontrol/AarfRateControl.ned | 1 + .../mac/ratecontrol/OnoeRateControl.ned | 1 + .../mac/ratecontrol/RateControlBase.cc | 13 +- .../mac/ratecontrol/RateControlBase.h | 5 +- .../Ieee80211PeerModeSelection.cc | 16 +- .../Ieee80211PeerModeSelection.h | 3 +- .../mac/rateselection/QosRateSelection.cc | 15 +- .../mac/rateselection/QosRateSelection.h | 6 +- .../mac/rateselection/QosRateSelection.ned | 1 + .../mac/rateselection/RateSelection.cc | 14 +- .../mac/rateselection/RateSelection.h | 6 +- .../mac/rateselection/RateSelection.ned | 1 + .../ieee80211/mac/recipient/CtsPolicy.cc | 2 +- .../ieee80211/mac/recipient/CtsPolicy.h | 4 +- .../ieee80211/mac/recipient/CtsPolicy.ned | 1 + .../ieee80211/mac/recipient/QosCtsPolicy.cc | 2 +- .../ieee80211/mac/recipient/QosCtsPolicy.h | 2 +- .../ieee80211/mac/recipient/QosCtsPolicy.ned | 1 + .../mac/recipient/RecipientAckPolicy.cc | 2 +- .../mac/recipient/RecipientAckPolicy.h | 4 +- .../mac/recipient/RecipientAckPolicy.ned | 1 + .../mac/recipient/RecipientQosAckPolicy.cc | 2 +- .../mac/recipient/RecipientQosAckPolicy.h | 4 +- .../mac/recipient/RecipientQosAckPolicy.ned | 1 + .../ieee80211/mgmt/Ieee80211AgentSta.cc | 2 +- .../ieee80211/mgmt/Ieee80211MgmtAdhoc.cc | 7 + .../ieee80211/mgmt/Ieee80211MgmtAdhoc.h | 1 + .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 53 +- .../ieee80211/mgmt/Ieee80211MgmtAp.ned | 3 +- .../ieee80211/mgmt/Ieee80211MgmtApBase.cc | 74 ++- .../ieee80211/mgmt/Ieee80211MgmtApBase.h | 10 +- .../mgmt/Ieee80211MgmtApSimplified.ned | 3 +- .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 111 ++-- .../ieee80211/mgmt/Ieee80211MgmtBase.h | 12 +- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 94 ++-- .../ieee80211/mgmt/Ieee80211MgmtSta.h | 1 + .../mgmt/Ieee80211MgmtStaSimplified.cc | 36 +- .../mgmt/contract/IIeee80211BssProvider.h | 23 + .../mgmt/contract/IIeee80211BssProvider.ned | 12 + .../ieee80211/mib/Ieee80211HtCapabilities.h | 20 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 291 +++++----- .../linklayer/ieee80211/mib/Ieee80211Mib.h | 48 +- .../linklayer/ieee80211/mib/Ieee80211Mib.ned | 1 + .../base/L3NetworkConfiguratorBase.cc | 2 +- .../contract/IIeee80211ReceiverCapabilities.h | 25 + .../IIeee80211ReceiverCapabilities.ned | 10 + .../IIeee80211TransmitterCapabilities.h | 24 + .../IIeee80211TransmitterCapabilities.ned | 10 + .../packetlevel/Ieee80211Receiver.cc | 5 + .../ieee80211/packetlevel/Ieee80211Receiver.h | 6 +- .../packetlevel/Ieee80211Receiver.ned | 4 +- .../packetlevel/Ieee80211Transmitter.h | 5 +- .../packetlevel/Ieee80211Transmitter.ned | 4 +- .../Ieee80211AgentStaReassociation_1.test | 14 +- .../Ieee80211ConfigurationContracts_1.test | 202 +++++++ .../Ieee80211HtAntennaRateControl_1.test | 24 +- tests/module/Ieee80211HtAssociation_1.test | 24 +- .../Ieee80211HtCapabilityPreparation_1.test | 157 ++++++ .../Ieee80211MgmtApChannelChange_1.test | 45 +- .../module/Ieee80211MgmtApGenericRadio_1.test | 2 +- .../module/Ieee80211MgmtApHcfQueueDrop_1.test | 8 +- .../Ieee80211MgmtApHcfRtsTimeout_1.test | 6 +- tests/module/Ieee80211MgmtApLifecycle_1.test | 22 +- .../Ieee80211MgmtApMalformedHtCap_1.test | 24 +- tests/module/Ieee80211MgmtApQueueDrop_1.test | 4 +- ...eee80211MgmtApReassociationSnapshot_1.test | 48 +- tests/module/Ieee80211MgmtApTimeout_1.test | 16 +- .../Ieee80211MgmtStaBeaconUpdate_1.test | 116 +++- .../Ieee80211MgmtStaDeauthentication_1.test | 22 +- .../Ieee80211MgmtStaDisassociation_1.test | 12 +- tests/module/Ieee80211MgmtStaDiscovery_1.test | 32 +- tests/module/Ieee80211MgmtStaLifecycle_1.test | 17 +- ...0211MgmtStaSimplifiedInitialization_1.test | 149 ++++- .../protocol/wifi/11n/WifiHtAssociation.test | 41 ++ tests/protocol/wifi/common/WifiDeauth.test | 4 +- tests/unit/Ieee80211HtCapabilities_1.test | 8 +- tests/unit/Ieee80211MibAssociationId_1.test | 12 +- tests/unit/Ieee80211PeerModeSelection_1.test | 63 ++- 134 files changed, 2584 insertions(+), 765 deletions(-) create mode 100644 plan/done/80211htcapop-refactor-v3-evidence.md delete mode 100644 src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc delete mode 100644 src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h create mode 100644 src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc create mode 100644 src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned create mode 100644 src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.h create mode 100644 src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.ned create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.h create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.ned create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.h create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.ned create mode 100644 tests/module/Ieee80211ConfigurationContracts_1.test create mode 100644 tests/module/Ieee80211HtCapabilityPreparation_1.test create mode 100644 tests/protocol/wifi/11n/WifiHtAssociation.test diff --git a/WHATSNEW b/WHATSNEW index 1a61a1b6935..9e29f5c5c19 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -280,6 +280,22 @@ Notable backward incompatible changes are the following: arrives, so renewal timing does not move; traffic from the client starts one probeWait later. +16. IEEE 802.11 capability and BSS state ownership + + HT capabilities are prepared independently of the active BSS operation. + Simplified associations prepare both peers before network configuration, + regardless of node declaration order. Accepted BSS changes publish a + bssStateChanged notification after management completes its bookkeeping. + Legacy simplified stations retain BSS identity and channel information + without accepting the HT operation advertised by an HT access point. + + Custom WLAN modules must replace ModeSetListener and modesetChanged with + an explicit modeSetModule provider, and replace direct MIB BSS/profile + writes with the owner APIs. Custom HT PHY contributors use typed capability + interfaces. The migration guide describes these source and wiring changes. + The selected legacy QoS and non-QoS ad hoc fingerprints remain unchanged; + no recorded fingerprint baseline was updated. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/project/design/ieee80211-model-architecture.md b/doc/project/design/ieee80211-model-architecture.md index 1a8f75f9199..7df73468377 100644 --- a/doc/project/design/ieee80211-model-architecture.md +++ b/doc/project/design/ieee80211-model-architecture.md @@ -92,3 +92,28 @@ Concrete signals declare their source/scope, change condition, payload, and life [AR-COM-NOTIFY](../rule/architecture.md#ar-com-notify). An immutable borrowed snapshot is allowed; it creates no second writable authority. Commands, queries, and required coordination use typed calls or protocol messages, rather than notifications. + +**Implemented HT contracts.** `IIeee80211MacConfiguration` exposes the configured catalog after +`LOCAL` and an idempotent `prepareLocalCapabilities()` operation after PHY readiness. MAC consumers +resolve their `modeSetModule` dependency through `ModeSetModuleBase` at `LINK_LAYER`; the MAC NED +provides the default descendant path. Management uses `macModule`. `LINK_LAYER` depends explicitly +on both `PHYSICAL_LAYER` and `NETWORK_INTERFACE_CONFIGURATION`, so addresses and contribution inputs +are available before simplified association. Its AP preparation/install/removal calls use +`IIeee80211BssProvider`; no remote `LAST` callback is required. + +`Ieee80211Mib` provides const BSS/profile queries and guarded mutations. Management stages a complete +BSS/peer change, finishes its transaction and timer bookkeeping, then calls `publishStateChange()`. +The MIB's `bssStateChanged` signal covers meaningful BSS or peer changes. Its Boolean value reports +whether a BSS is active; observers query the current MIB for details. There is no borrowed signal +payload. Delivery is synchronous and observational: MIB mutation during delivery throws an error. +References into the MIB are valid only until the next corresponding mutation. A retained +`shared_ptr` keeps an immutable capability result alive, +but does not grant continued relationship eligibility. + +`hasPreparedLocalCapabilities()`, `isLocalHtCapable()`, `hasActiveBss()`, `hasHtOperation()`, and +`relationshipAllowsHt(peer)` distinguish readiness, implementation support, current BSS presence, +accepted operation presence, and permission to select HT. A legacy BSS may be active without HT +operation. The current ad hoc no-beacon abstraction likewise has no learned channel/HT operation or +accepted peer advertisements. Stop/crash clears operational relationships while retaining prepared +configuration. Physical AP channel context is retained by management for restart; STA operation is +learned from accepted management information, independently of scan tuning. diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 668ccf48149..a8f16c4fee1 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -4,6 +4,48 @@ Migrating Code from INET 3.x ============================ Release: |release| +IEEE 802.11 Capability and BSS State Ownership +-------------------------------------------- + +Custom modules that used ``ModeSetListener`` or subscribed to ``modesetChanged`` +must obtain the configured catalog through ``IIeee80211MacConfiguration``. +For catalog consumers, derive from ``ModeSetModuleBase``, declare a +``modeSetModule`` NED parameter, and call the base initialization before using +``modeSet`` at ``INITSTAGE_LINK_LAYER``. Keep ``NUM_INIT_STAGES``. The built-in +MAC supplies the descendant parameter default; standalone consumers must point +it at a module implementing the C++ and NED configuration contracts. Move +algorithm initialization formerly performed by the signal callback to that +initialization stage. Ordinary catalog queries must not reset algorithm state. + +Custom transmitters and receivers that contribute HT capabilities implement +``IIeee80211TransmitterCapabilities`` and ``IIeee80211ReceiverCapabilities``, +respectively, in C++ and NED. Report implemented width support and, for receivers, +short-GI support independently of the current BSS operation. The MAC's +``prepareLocalCapabilities()`` assembles the local profile after PHY readiness; +repeated preparation preserves active protocol state. + +Replace direct access to MIB BSS/profile fields with ``getBssData()``, +``getBssStationData()``, ``getBssAccessPointData()`` and +``getLocalHtCapabilities()`` for reads. Management owners use ``commitBss()``, +``clearBss()`` and the association/peer methods for writes. Profile installation +uses ``installLocalHtCapabilities()``; replacing a changed profile requires an +inactive BSS with no peer relationships. Custom simplified AP management +implements ``IIeee80211BssProvider`` for explicit preparation and peer +installation/removal instead of allowing another module to modify its MIB. + +Subscribe to the MIB's Boolean ``bssStateChanged`` signal to observe accepted +state transitions. Query the MIB synchronously; the signal carries no borrowed +state object. Management calls ``publishStateChange()`` after completing the +transition's timer and transaction bookkeeping. Observers must not mutate the +MIB during publication or retain raw BSS/peer references across a mutation. +Use ``hasActiveBss()`` and ``hasHtOperation()`` explicitly, and supply operation +and HT eligibility separately from cached capabilities to peer mode selection. + +The old initialization signal and writable BSS/profile fields are removed +without compatibility adapters because they permit missed initialization or +state changes that bypass owner bookkeeping and notification. Existing custom +implementations must migrate together with their consumers. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/plan/done/80211htcapop-refactor-v3-evidence.md b/plan/done/80211htcapop-refactor-v3-evidence.md new file mode 100644 index 00000000000..9e30289619b --- /dev/null +++ b/plan/done/80211htcapop-refactor-v3-evidence.md @@ -0,0 +1,266 @@ +# HT capability/BSS refactor V3: implementation evidence + +Date: 2026-09-18. Baseline: `98117c3257b2e11661d2baf685c18911c8639b24`. +Tested implementation: the uncommitted working tree following that baseline. No commits, recorded +fingerprints, generated fingerprint expectations, or sealed packet-core sources were changed. +The proposal's relevant source/test paths match baseline `3f89b4b439c1dafd6b217ae9beb94b604f50c615`. + +## Implemented decisions + +- MAC assembles capabilities through `IIeee80211TransmitterCapabilities` and + `IIeee80211ReceiverCapabilities`, configured catalog and antenna limits. Preparation is idempotent; + MIB installation does not construct operation. A changed profile cannot replace an active BSS or + installed peer relationship. Ordinary runtime profile reconfiguration is outside this change. +- Management constructs operation and owns accepted transitions. MIB stores an explicit active BSS, + decoding band/channel context, optional operation, accepted capabilities and association state. + Its BSS/profile structures are private, exposed through const queries and guarded writes. +- The AP retains physical channel context while down, reconstructs operation on restart, and uses + its current operation after response completion. STA response/discovery provenance remains local + to management. Scan tuning is not used as accepted BSS state. +- `bssStateChanged` is a MIB Boolean signal with no borrowed payload. Management publishes after + required timer/transaction work. Observers query the committed MIB; nested MIB mutation is rejected. + Equality suppresses redundant state notification, not accepted-beacon liveness refresh. +- Directional capability results are immutable shared values. Equal capability inputs reuse them; + operation-only changes reevaluate eligibility without deriving again. Teardown and acknowledged + relationship replacement remove the old cache. A retained shared pointer proves reuse/lifetime in + tests without adding a public counter. Raw peer/BSS references must not survive their next mutation. +- Both selection modules pass accepted capabilities, explicit operation and HT eligibility to the + common compatibility filter. Management/control/group dispatch and legacy fallback remain intact. +- Static catalog broadcasts and `ModeSetListener` are removed. The replacement base queries a declared + provider at `LINK_LAYER`; the MAC supplies `**.modeSetModule` defaults to descendants in either + standard or external-upper composition. Management retains its existing `macModule` parameter. +- `LINK_LAYER` now explicitly depends on `NETWORK_INTERFACE_CONFIGURATION`, as well as PHY readiness. + Simplified management prepares the AP through `IIeee80211BssProvider`, then installs local and + remote state through owner methods. Identity and HT state are ready before network configuration. +- Ad hoc management explicitly activates/clears its no-beacon abstraction without inventing learned + channel, HT operation, or peer advertisements. Prepared local HT/legacy support survives lifecycle. + +The concrete notification and lifetime contracts are documented in +[the WLAN architecture](../../doc/project/design/ieee80211-model-architecture.md). + +## Consumer readiness worksheet + +For all catalog consumers below, `modeSetModule` identifies a module implementing +`IIeee80211MacConfiguration`. The default is the enclosing MAC's absolute path, set by MAC NED; +standalone compositions specify their own path. Catalog selection is ready after all `LOCAL` +callbacks. `ModeSetModuleBase::initialize(LINK_LAYER)` obtains it before the derived callback. +All retain `NUM_INIT_STAGES`. Catalog acquisition performs no algorithm reset on ordinary queries. + +| Consumer | Additional prerequisite / preparation at LINK_LAYER | First read / lifetime rule | +|---|---|---| +| `Dcaf` | Resolve queue/contention/Rx and calculate slot/SIFS/IFS/EIFS/CW defaults | Contention request; preserve subsequent CW changes | +| `Edcaf` | AC/parameter inputs initialized at LOCAL; register contention and calculate timing/CW | Contention request; preserve subsequent CW changes | +| `Dcf` | Existing coordination references | Protocol actions; retain unrelated drop listener | +| `Hcf` | Existing coordination references | Protocol actions; retain unrelated drop listener | +| `RateSelection` | Resolve control and configured frame modes; fastest mandatory mode | Frame selection; preserve transmitted-frame history | +| `QosRateSelection` | Resolve control and configured frame modes; fastest mandatory mode | Frame selection; preserve per-receiver/frame history | +| `AarfRateControl` | Concrete LOCAL parameters; base invokes `resetRateControl()` | Initial mode / first station estimate; no repeated preparation reset | +| `OnoeRateControl` | Concrete LOCAL parameters; base invokes `resetRateControl()` | Explicit first rate query at NETWORK_CONFIGURATION in the configuration-contract fixture; no repeated preparation reset | +| `OriginatorAckPolicy` | Catalog query | ACK-policy timing during exchange | +| `OriginatorQosAckPolicy` | Catalog query | QoS ACK-policy timing during exchange | +| `RtsPolicy` | Catalog query | RTS decision/timing | +| `QosRtsPolicy` | Catalog query | QoS RTS decision/timing | +| `RecipientAckPolicy` | Catalog query | Response timing | +| `RecipientQosAckPolicy` | Catalog query | QoS response timing | +| `CtsPolicy` | Catalog query | CTS timing | +| `QosCtsPolicy` | Catalog query | QoS CTS timing | +| `TxopProcedure` | Catalog query | TXOP timing; later TXOP state belongs to procedure | +| `SingleProtectionMechanism` | Catalog query | Protection timing | +| `OriginatorProtectionMechanism` | Catalog query | Originator protection timing | +| `OriginatorBlockAckAgreementPolicy` | Catalog query | Agreement policy; agreement lifecycle remains with existing owners | +| `Ieee80211MgmtBase` | Existing `macModule`; idempotent MAC profile preparation and supported-rate construction | First management frame/peer derivation; no repeated reset | +| AP management | PHY channel notification retained; profile + configured operation policy | Explicit `prepareBss()` or LINK_LAYER, before advertisement/peer installation | +| Simplified STA management | Both interface addresses after NETWORK_INTERFACE_CONFIGURATION; both profiles; explicit AP preparation | LINK_LAYER, before NETWORK_CONFIGURATION; no LAST retry | +| Ad hoc management | Prepared local profile | LINK_LAYER/start; no learned HT operation is fabricated | + +`FrameSequenceContext` keeps a const catalog pointer supplied by its owner. It does not subscribe to +an initialization event. `opMode`, `mac.modeSet`, radio contribution parameters, and `macModule` paths +retain their forwarding; the new descendant default also covers external-upper composition. + +## Acceptance evidence + +| Claim | Production path and regression assertion | +|---|---| +| Independent, replaceable contribution assembly | `Ieee80211HtCapabilityPreparation_1`: production MAC queries a non-built-in transmitter wrapper and receiver contribution; widths/GI differ as declared; repeated preparation and installation preserve operation | +| Built-in advertisement/antenna limits | `Ieee80211HtAssociation_1`, `Ieee80211HtAntennaRateControl_1`, serialization/unit cases: real 20 MHz packet PHY and existing advertisement assertions | +| Directional support and bounded selection | Five selected unit cases plus beacon fixture invoking both production selection modules | +| Cache reuse and HT recovery | Beacon fixture: width-only/equal inputs retain shared result; changed GI/MCS refresh; missing HT removes; unsupported Basic MCS retains knowledge; Basic-MCS-only recovery reuses cache | +| Liveness and atomic STA notification | Two synchronous listeners, reversed registration order, inspect identity/band/operation/timer; equal Beacon refreshes deadline without notification; malformed Beacon/probe does not publish authoritative changes; nested clear is rejected | +| AP current operation versus response history | Reassociation snapshot fixture changes radio channel while response is pending; response bytes retain their captured value and MIB uses current channel | +| AP completion/replacement boundary | Same fixture observes pending status at channel change and cleared transaction/AID/peer state at completion; second equal-capability reassociation replaces the cache while retaining the correct AID | +| Scoped cleanup and lifecycle | Detailed AP/STA lifecycle, deauth/disassoc, timeout, queue drop, HCF queue/RTS timeout, malformed HT, discovery and agent reassociation fixtures | +| Early readiness independent of declaration order | Simplified initialization fixture contains both AP-first and STA-first pairs, checks before NETWORK_CONFIGURATION, repeats preparation and checks cache preservation; stop/crash/restart and missing-AP cleanup retained | +| Declared providers/compositions | `Ieee80211ConfigurationContracts_1`: standalone replacement catalog provider, missing-provider diagnostic, HT/legacy ad hoc stop/restart, inherited production external-upper composition with only TAP endpoint replaced before child initialization | +| Real peer sequences | `WifiAssociation`, new `WifiHtAssociation`, `WifiReassociation`, and migrated `WifiDeauth`: protocol-category packet exchange expectations | +| Legacy trajectory preservation | Two selected QoS/non-QoS ad hoc fingerprints, run 0, all three selected ingredients match recorded expectations | + +The 40/20 MHz transition fixtures are synthetic management/selection evidence. They do not establish +real 40 MHz packet-PHY support. The external-upper fixture does not claim real TAP I/O. One pinned +run/seed is used for deterministic transitions; no throughput/performance or standards-expansion +claim is made. + +## Commands and results + +All commands run from the repository root, except fingerprint commands from `tests/fingerprint`. +The active toolchain is OMNeT++ 6.4.0aipre2 / clang with the checkout's enabled features. Debug is the +behavioral test mode. Module/protocol tests use their checked-in bounded configuration, run 0 and +seed 0/default; protocol base explicitly pins `seed-set = 0`. + +Exact selectors: + +```bash +UNIT='Ieee80211(HtCapabilities_1|HtMgmtElements_1|HtModeSet_1|PeerModeSelection_1|MibAssociationId_1)\.test' +MODULE='Ieee80211(HtAssociation_1|HtAntennaRateControl_1|HtCapabilityPreparation_1|ConfigurationContracts_1|MgmtStaBeaconUpdate_1|MgmtApReassociationSnapshot_1|MgmtStaSimplifiedInitialization_1|MgmtAp(Lifecycle|Timeout|QueueDrop|HcfQueueDrop|HcfRtsTimeout|ChannelChange|GenericRadio|UnavailableChannel|MalformedHtCap)_1|MgmtSta(Lifecycle|Deauthentication|Disassociation|Discovery)_1|AgentStaReassociation_1)\.test' +make -j$(nproc) MODE=debug +inet_run_unit_tests -m debug -f "$UNIT" +inet_run_module_tests -m debug -f "$MODULE" +inet_run_module_tests -m debug -f 'Ieee80211MgmtStaBeaconUpdate_1\.test' +inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'Wifi(HtAssociation|Association|Reassociation)\.test$' +inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiDeauth\.test$' +make -j$(nproc) MODE=release +# cwd: tests/fingerprint; two configurations, 10s simulated time, run 0 +./fingerprinttest -d -m '/examples/adhoc/qos/ .* -c Mac(NonQos|Qos) -r 0 ' -f tplx -f '~tNl' -f '~tND' examples.csv +``` + +| Run | Executed | Outcome / log | +|---|---:|---| +| Baseline debug build | 1 | PASS, `/tmp/htcapop-baseline-build.log` | +| Baseline existing unit / core / lifecycle-module checks | 4 / 5 / 14 | PASS, `/tmp/htcapop-baseline-*` | +| P1/P2/P3 builds and focused migration checks | See implementation record | Corrected failures described below; `/tmp/htcapop-p1-*`, `p2-*`, `p3-*` | +| Final debug build | 1 | PASS, `/tmp/htcapop-final2-debug.log` | +| Final unit suite | 5 | PASS, `/tmp/htcapop-final2-unit.log` | +| Final module suite | 21 | PASS, `/tmp/htcapop-final2-module.log` | +| Added Basic-MCS-only recovery assertion | 1 existing module case | PASS, `/tmp/htcapop-basic-recovery.log` | +| Final association / HT association / reassociation protocol cases | 3 | PASS, `/tmp/htcapop-final2-protocol.log` | +| Migrated deauthentication protocol case | 1 | PASS, `/tmp/htcapop-deauth-protocol.log` | +| Release build | 1 + final incremental build | PASS (including final rebuilds); `/tmp/htcapop-final-release.log`, `/tmp/htcapop-final2-release.log` | +| Legacy fingerprints | 2 | PASS (initial and final-tree repeat), `/tmp/htcapop-final2-fingerprint.log` | + +Baseline unit/module evidence was captured before implementation. The protocol/fingerprint selectors +were finalized and executed later, a sequencing deviation from P0; fingerprint comparison uses the +unchanged repository expectations. An initial fingerprint regex anchored before trailing tags +selected zero cases (`NOT_RUN`); removing that anchor selected exactly the two intended cases. + +During development, tests exposed and drove corrections to missing-channel error precedence, AP +restart operation reconstruction, synthetic discovery-frame provenance, and lost fastest-mandatory +rate preparation after signal removal. These are resolved; their failed logs are retained rather +than presented as passes. Generated test build/work files were not used as source edits. + +## Mechanical gates and review boundaries + +- Scoped architecture gates for linklayer WLAN, PHY WLAN and external-upper WLAN pass. +- Full architecture gate reports 11 application/transport dependency candidates. An archived HEAD + comparison finds the identical 11; no added/removed candidate. +- Full interface gate reports 15 interface-purity violations. Archived HEAD has the identical 15; + all four new paired contracts satisfy the mechanical purity check. +- Scoped naming reports 35 declaration candidates in eight files. Each candidate-bearing file is + byte-identical to HEAD. The project-wide naming gate additionally reports existing resource names. + Its `--base HEAD` declaration scan selects zero committed changes, so scoped scans are the useful + evidence for this uncommitted implementation. +- Document seal/index gate passes. The source-seal `--base HEAD` gate selects no committed paths; + explicit working-tree/untracked-path inspection confirms no change under sealed `common/packet/`. +- Commit/classification checks are not applicable to an uncommitted working tree. The empty-range + commit checker emits a spurious empty-subject violation; the classification checker selects zero + commits. Neither is reported as source verification or a validated future commit series. +- C++ quality scan: stable scan completed on 36 changed translation units, with zero compiler errors. + Ten changed-line redundant-virtual diagnostics were corrected. The final rescan has zero + diagnostics on changed/new lines (4,079 unique diagnostics elsewhere in included legacy code). + The compilation database is generated from the + active debug build's exact compiler options at `/tmp/htcapop-compile-db/compile_commands.json`. +- Semantic self-review covers both selection callers, every old listener, full/simplified/ad hoc + lifecycle ownership, transaction matching and snapshot provenance. No independent reviewer was + commissioned, and repository-wide legacy gate violations are not silently waived or changed. + +## Semantic self-audit + +This is the author's final diff audit, not an independent code-review verdict. + +| General checklist item | Result and basis | +|---|---| +| AR-ORG-CONTRACTS | PASS — replacement contributors and standalone catalog provider exercise the declared query outcomes | +| PR-MSG-BODY / PR-MSG-WHY | N/A — no new commits authored | +| AR-ORG-CONTRACT-PURITY | PASS — four role interfaces contain pure operations and trivial destructors only | +| AR-ORG-VIS-SPLIT | PASS — model state remains in MIB; visualizer changes are const-reader migration | +| AR-ORG-KERNEL | PASS — adds an edge to the existing INET initialization-stage graph | +| AR-MOD-COMPOSITION | PASS — provider interfaces use existing modules; shared initialization base contains only dependency acquisition | +| AR-COM-SOCKETS | N/A — no application/transport integration | +| AR-COM-DIRECT | PASS — readiness uses direct typed calls; no zero-time handshake event | +| AR-COM-NOTIFY | PASS — committed state notification after timers/transactions, no required subscriber | +| AR-OBS-SIGNALS | PASS — synchronous observation and rejected nested state mutation tested in both registration orders | +| AR-OBS-NED-TRUTH | PASS — dependency defaults and signal declaration live in NED; documentation adds readiness/lifetime semantics | +| AR-OBS-INTROSPECTION | N/A — no new wire protocol or packet representation | +| AR-CFG-INFER / QR-DUP | PASS — remove copied operation and derive eligibility from accepted state | +| QR-OBJECT-OWNERSHIP | PASS — immutable shared cache, retained test snapshots, existing timer/packet cleanup paths | +| AR-CFG-PARAMS | PASS — new provider paths default empty and are mandatory dependencies, filled by composition | +| AR-EXT-NOCORE | N/A — no new protocol registration; common edits remove a signal and declare readiness | +| AR-EXT-MINIMAL-SURFACE | PASS — state private; public queries/preparation/commit operations serve production roles; old eligible-peer query retained as a const compatibility query | +| AR-EXT-VIRTUAL-IS-A-PROMISE | PASS — provider roles, framework hooks, and AP/ad hoc operation template step | +| AR-BUILD-DECLARATIVE | PASS — no machine paths or flags added to production build descriptors | +| RR-NUMERIC-STABLE | N/A — no enum/numeric wire code changed | +| AR-QUAL-NAMING | PASS — new role/type/parameter names follow role and camelCase conventions; existing findings separately identified | +| AR-QUAL-LOGGING | PASS — invalid preparation/dependency/mutation throws; no logged invariant failure proceeds | +| AR-QUAL-DETERMINISM | PASS — semantic equality and existing deterministic mode tie-breaks; pointer identity used only for test cache observation | +| AR-QUAL-TESTS | PASS — focused unit, module, protocol and legacy fingerprint evidence | +| AR-QUAL-TRACEABILITY | N/A — no recorded fingerprint/statistical baseline changed | +| AR-QUAL-DISPLAY | N/A — no new concrete production NED module; existing MIB inspection retained | + +| WLAN checklist item | Result and basis | +|---|---| +| AR-WLAN-STD-TRACE | PASS — existing normative references retained; no-air/ad hoc behavior explicitly identified as abstraction | +| AR-WLAN-STD-GATING | PASS — local profile, accepted capabilities, operation and eligibility remain separate gates | +| AR-WLAN-ARCH-BOUNDARIES | PASS — capability assembly uses role contracts; simplified STA calls AP owner operations | +| AR-WLAN-ARCH-OWNERSHIP | PASS — current BSS operation has one writer; discovery/pending snapshots retain their historical purpose | +| AR-WLAN-ARCH-VARIANTS | PASS — AP/ad hoc operation uses existing management roles | +| AR-WLAN-FRAME-REPRESENTATION | N/A — no new on-air fields/tags or serializer changes | +| AR-WLAN-PHY-AUTHORITY / AR-WLAN-PHY-TIMING | PASS — band/mode APIs remain authority for legality and timing | +| AR-WLAN-MAC-EXCHANGE | PASS — no new exchange matcher; management continues receiving existing transmission outcomes | +| AR-WLAN-MAC-SEQUENCE | N/A — no sequence/window arithmetic change | +| AR-WLAN-MAC-QOS | PASS — existing EDCA state and callback timing preparation retained | +| AR-WLAN-MAC-MULTIUSER | N/A — no MU feature work | +| AR-WLAN-OBS-EVENTS | PASS — equal information does not republish state; protocol outcomes retain their distinct signal | +| AR-WLAN-QUAL-TESTS | PASS — acceptance matrix above, including legacy fingerprints | + +REVIEW: 29 PASS, 10 N/A, 0 FLAG, 0 QUESTION. +Mechanical gate failures and test boundaries are recorded separately above. + +## Final review corrections + +- `prepareBss()` rejects calls while AP management is down; AP lifecycle coverage asserts that + preparation cannot resurrect cleared state. Both it and simplified initialization pass after the + guard (`/tmp/htcapop-inactive-ap.log`). +- New catalog-path NED parameters have explicit empty defaults; standard composition supplies the + mandatory path, and standalone missing-provider behavior remains an error. New contracts/base + follow the checker's namespace, guard, trivial-destructor and override conventions. A transient + namespace edit failed compilation and was fixed before the final builds. +- Fresh final debug/release builds pass (`/tmp/htcapop-final5-debug.log`, + `/tmp/htcapop-final5-release.log`). Three provider/contribution/composition cases pass afterward + (`/tmp/htcapop-provider-defaults.log`). Final-tree legacy fingerprints pass, two cases / three + ingredients each (`/tmp/htcapop-final2-fingerprint.log`). +- The long whole-WLAN C++ scan overlapped header edits and is invalid as final evidence. The final + stable-source scan uses the same compilation database/check configuration on every changed `.cc` + in six parallel processes, preserving one log per translation unit under `/tmp/htcapop-tidy-final`. + A focused new-provider/base scan reports no diagnostic in those new files + (`/tmp/htcapop-cpp-new-provider.log`; included legacy headers still generate suppressed warnings). + +## Completion record + +Final debug and release builds: PASS, `/tmp/htcapop-final-debug-build.log` and +`/tmp/htcapop-final-release-build.log`. Both Aarf and Onoe focused cases pass after the final +header/default cleanup (`/tmp/htcapop-final-ratecontrol.log`). The final stable C++ scan covers +36 changed translation units: zero tool/compiler errors, zero diagnostics on changed/new lines; +4,079 unique diagnostics remain elsewhere in included legacy code. These are not represented as a +clean repository-wide lint result. See `/tmp/htcapop-tidy-final-summary.log` and +`/tmp/htcapop-tidy-final-diff.log`. All new contracts/base files are included in the changed-line +filter regardless of line number. + +The full final 21-module / 5-unit / 4-protocol runs precede only the explicitly recorded inactive-AP +guard and declaration/default cleanup; affected lifecycle, composition, contribution, and rate-control +cases were rerun afterward. The guard does not enter the ad hoc fingerprint paths. Final fingerprint +and code-style changes do not modify recorded expectations. `git diff --check` is clean. + +Logs and the per-translation-unit C++ results are preserved locally under +`report/htcapop-v3/logs/` in addition to their original `/tmp` paths. This evidence directory follows +the checkout's existing ignored-report convention; the plan/evidence Markdown files are reviewable +new files under `plan/done/`. No commit or push was performed. + +Source/test manifest SHA-256: `7cd8705f802b9f599c76ad696cef7daffdc5b57e2a2a0a874c7c7d58ce129375` (130 paths, including deletions). diff --git a/plan/done/80211htcapop-refactor-v3.md b/plan/done/80211htcapop-refactor-v3.md index 4e26ac707b4..a1bf2cd6491 100644 --- a/plan/done/80211htcapop-refactor-v3.md +++ b/plan/done/80211htcapop-refactor-v3.md @@ -1,5 +1,519 @@ -# IEEE 802.11 capability and BSS ownership plan +# Implementation plan: IEEE 802.11 HT capability, BSS, and peer-state contracts -Status: planned. +> **Status:** complete · **Source:** [Proposal V3](../../report/80211htcapop-refactor-v3.md) · **Prepared:** 2026-09-18 -Prepare interface identity before link-layer consumers initialize. Separate local capability ownership from mutable BSS state, prepare HT capabilities before management startup, and expose a read-only mode-set provider. Preserve effective configuration contracts and validate focused capability, association, discovery and initialization fixtures. Record completed evidence with implementation. +Deliver the four contracts in V3 through four coherent implementation milestones: independent +capability preparation, committed management transitions, capability-only peer caching, and explicit +initialization dependencies. Preserve the existing HT and legacy behavior described by the proposal. +Implementation results and the consumer readiness worksheet are recorded in the +[execution evidence](80211htcapop-refactor-v3-evidence.md). The sections below preserve the original +work breakdown; the evidence records actual commands, outcomes, boundaries, and deviations. + +## 1. Baseline, scope, and deliverables + +The proposal references `3f89b4b439c1dafd6b217ae9beb94b604f50c615`. This plan was prepared against +`98117c3257b2e11661d2baf685c18911c8639b24`. A comparison of those commits found no differences in +`src/inet/linklayer/ieee80211`, `src/inet/physicallayer/wireless/ieee80211`, `tests/unit`, or +`tests/module`. Refresh this comparison before implementation, including newly added amendment +writers and consumers. The working tree was clean before this plan was added. + +At planning time, the source exhibited the migration points identified by V3: + +- `Ieee80211Mac::initialize()` selects the catalog at `LOCAL`, casts to concrete PHY contributors + at `LINK_LAYER`, prepares MIB capabilities, and emits `modesetChanged`. +- `Ieee80211Mib::updateLocalHtCapabilities()` also resets operation, derives its basic MCS set, + applies operation configuration, and rebuilds peer results. +- `Ieee80211Mib::setPrimaryChannel()` chooses width fallback and rebuilds peer results. +- `Ieee80211NegotiatedHtCapabilities` contains an operation copy read by the shared selector. +- `Ieee80211MgmtApBase` finalizes operation at `LAST`; simplified STA management calls + `configureAssociation()` at both `LINK_LAYER` and `LAST`. + +Implementation deliverables are: + +1. Typed, read-only catalog and component-contribution contracts with compatible NED wiring. +2. A prepared local capability profile, explicit active BSS state, and relationship-scoped peer state. +3. Management-owned transition operations with complete-state notification and lifecycle behavior. +4. A shared HT compatibility filter consuming cached capabilities, eligibility, and explicit operation. +5. A verified consumer preparation map and removal of initialization signal/listener plumbing. +6. Focused regression tests, an evidence manifest, and removal of temporary migration adapters. + +Use the [architecture](../../doc/project/rule/architecture.md), +[WLAN architecture](../../doc/project/design/ieee80211-model-architecture.md), +[WLAN rules](../../doc/project/domain/ieee80211.md), +[testing rules](../../doc/project/rule/testing.md), and +[contribution workflow](../../doc/project/guide/contribute-a-change.md) as the governing references. +Recheck the [seal registry](../../doc/project/audit/seal-list.md) before source edits. The currently +listed `common/packet/` seal is outside the intended change surface; no packet-core edit is planned. +Check the existing architecture and naming exception ledgers before reporting a deviation. + +### Compatibility boundaries + +Preserve parameter paths and precedence, built-in advertisements, association outcomes, supported-rate +elements, rate-selection behavior, frame-class dispatch, legacy operation, and existing radio command +handling. Keep protocol state inspectable in simple modules through WATCH/display facilities. + +Explicitly exclude new capability switches, new PHY features, complete basic-rate policy, runtime +interface-wide mode reconfiguration, Notify Channel Width, coexistence scheduling, operating-class +transitions, and VHT/HE/EHT capability models. Do not turn a serialized field into an implementation +claim. Do not claim a performance or standards-conformance improvement from this refactor alone. + +Retain the standards basis in V3. Its STA fallback/recovery, current-AP Probe Response treatment, +simplified no-air association, and immediate local channel updates are compatibility/model choices. +If implementation needs a new normative decision, verify the cited standard and scope that decision +separately rather than silently changing the expected behavior. + +## 2. Dependency order and review boundaries + +```text +P0: baseline, writer/reader inventory, API and readiness design + -> P1: typed contributions + capability-only preparation + replacement operation initializer + -> P2: committed BSS/relationship transitions + notification + lifecycle + -> P3: capability-only cache + explicit selection context + remove operation copies + -> P4: typed catalog dependency + migrate all preparation + remove initialization broadcast + -> P5: final integration evidence, cleanup, documentation, review +``` + +Each milestone must build and have its directly related tests passing before the next milestone +relies on it. Add tests alongside each behavior migration. Keep broad renames and formatting out of +semantic commits. A milestone can contain several commits, but every commit must leave a coherent, +buildable model. In particular, remove an old initializer only in a commit that supplies its +replacement, and migrate a selector signature together with its callers. + +| Milestone | Suggested commit concern | Temporary compatibility mechanism | Removal deadline | +|---|---|---|---| +| P0 | Characterization tests for currently supported behavior, if missing | Existing model | Before semantic changes | +| P1 | Query contracts, built-in contributors, separated local construction and operation preparation | Existing initialization signal; selector operation copies | P3/P4 | +| P2 | Shared transition contract, then detailed and simplified role migration | Old selector input adapted from the committed view | P3 | +| P3 | Cache/selection migration and obsolete state removal | Initialization signal only | P4 | +| P4 | Catalog provider and consumer migrations in buildable groups; signal removal last | Short-lived provider/signal coexistence | End of P4 | +| P5 | Remaining integration fixtures and contract documentation | None | Completion | + +## 3. P0 — establish the execution baseline + +**Entry:** implementation checkout and target revision are identified. + +- [x] Record HEAD, working-tree status, build environment, enabled INET features, and the proposal + baseline comparison. Preserve unrelated local changes. **Done:** baseline recorded; only this task + changed the initially clean source. +- [x] Inventory all readers/writers of local capabilities, `PeerHtState`, BSS identity, operation, + `isHtOperationSupported()`, `setPrimaryChannel()`, and `negotiateHtCapabilities()` across the tree. + Classify each as configuration, management policy, shared storage, transaction snapshot, or algorithm + state. Include tests, serializers, simplified/ad hoc paths, and any concurrent amendment additions. +- [x] Inventory every subscription, override, and inherited dependency on `ModeSetListener` and + `modesetChangedSignal`. Use the starting inventory in section 8, then search the whole source tree. +- [x] Record effective `opMode`/`modeSet` forwarding in both interface compositions and standalone + fixtures. Include default forwarding, an explicit MAC catalog override, radio/component parameters, + and initialization errors. Preserve these paths instead of consolidating them speculatively. +- [x] Complete the API decisions in section 4 and the readiness worksheet in section 8. Settle contract + placement before adding a dependency that would point PHY code back into MAC implementation types. +- [x] Build a fresh debug library and run the existing focused tests in section 10. Record pre-existing + failures separately. Capture exact advertisements, accepted state, selection results, and current + rejection behavior where later comparisons need an oracle. +- [x] Select the directly affected legacy fingerprint cases and peer-exchange protocol cases. Record + their exact selectors and executed counts before production changes; avoid choosing cases merely + because they pass. **Sequencing deviation:** protocol/fingerprint selection and execution followed + the initial source changes; unit/module baseline runs preceded them. See execution evidence. + +Useful inventory commands, run from the repository root: + +```bash +git rev-parse HEAD +git status --short +rg -n 'ModeSetListener|modesetChangedSignal|modesetChanged' src tests +rg -n 'updateLocalHtCapabilities|setPeerHtCapabilities|findPeerHtState|negotiateHtCapabilities' src tests +rg -n 'localHtCapabilities|bssData|bssStationData|bssAccessPointData|getHtOperation|setPrimaryChannel' src/inet/linklayer/ieee80211 +rg -n 'INITSTAGE_|Define_InitStage_Dependency' src/inet/common/InitStages.cc src/inet/linklayer/ieee80211 +``` + +**Exit:** a finite reader/writer list, exact baseline test selection, parameter-precedence record, and +resolved preparation design exist. New-contract tests may be planned here and implemented in their +own milestone; the baseline suite must describe the old supported behavior rather than require the +new API to exist. + +## 4. Contract decisions to settle before implementation + +The following are recommended shapes, not mandatory new class names. Prefer extending suitable +existing value types over creating one class or module per concept. + +| Contract | Required content and invariant | Decision owner | +|---|---|---| +| Configured catalog provider | Const query such as `getConfiguredModeSet()`; configuration lifetime; explicit readiness | MAC | +| Tx/Rx contribution | Typed queries for currently needed implemented abilities; identify direction, widths, stream/MCS limits, and per-width receive short GI | Implementing PHY component | +| Prepared local profile | Distinguish unprepared, prepared legacy, and prepared HT; stable across ordinary stop/restart | Assembly installs; MIB stores | +| Current BSS | Presence, identity, role-appropriate channel context, optional HT operation; legacy channel information usable independently | Management decides; MIB stores | +| Relationship | Explicit installed relationship, accepted peer information, validated HT eligibility; address equality alone is not identity | Management decides; MIB stores | +| Directional cache | Exact capability-derived results for local-Tx/peer-Rx and local-Rx/peer-Tx; immutable to consumers | MIB peer record | +| Transition/publication | Validate before mutation; commit complete shared state; finish owner bookkeeping; publish once | Management coordinates; MIB emits shared-state notification | + +Choose these details explicitly: + +- Keep known directional capabilities during temporary operational HT ineligibility, with all use + gated by the validated relationship query. An accepted missing/changed advertisement must still + update its presence/contents appropriately; retained historical information cannot become current + merely because eligibility later changes. Teardown discards relationship-scoped state. +- Treat local capability installation as initialization-only in ordinary operation. Repeated typed + preparation is idempotent; an unsupported attempt to replace a prepared profile must not leave + stale peer caches. Do not add runtime reconfiguration as part of this work. +- Compute semantic equality across every derivation input, preserving presence and unknown values. + Do not compare padded structures with `memcmp`, or compare only an MCS ceiling. +- Keep band plus internal channel index together, or provide equivalent unambiguous decoding context. + Preserve a generic legacy radio's supported absence of an IEEE band mapping. +- Distinguish `hasPreparedLocalCapabilities`, local HT support, active BSS presence, and applicable + HT operation in query semantics. Final names should follow repository naming guidance. Remove the + ambiguous MIB query once callers migrate; the similarly named mode-catalog query has a different role. +- Define const-reference/pointer lifetimes: a peer pointer may become invalid on teardown or replacement. + Selection should consume one consistent view without retaining it across callbacks. +- Keep pending responses and discovery snapshots immutable where their historical meaning requires it. + Eliminating duplicated current operation does not authorize deleting transaction history. + +## 5. P1 — separate capability construction and management operation + +**Primary files:** `mac/Ieee80211Mac.{h,cc}`, `mib/Ieee80211Mib.{h,cc,ned}`, +`mib/Ieee80211HtCapabilities.h`, `mgmt/Ieee80211MgmtApBase.{h,cc}`, relevant ad hoc management, +and PHY contributor declarations/implementations under +`src/inet/physicallayer/wireless/ieee80211/packetlevel/`. +Paths beginning `mac/`, `mib/`, or `mgmt/` here and below are relative to +`src/inet/linklayer/ieee80211/`. + +- [x] Add the smallest typed contribution contracts in the package owning the queried role. Keep + interface declarations pure; place implementation in concrete/base classes. Avoid making PHY + depend on a MAC-owned profile assembler or requiring one concrete radio class. +- [x] Implement the contracts in the built-in transmitter/receiver. Document each contribution's + source, direction, implementation limit, readiness, and consumer. Preserve built-in answers. +- [x] Replace the MAC's casts to concrete `Ieee80211Transmitter`/`Ieee80211Receiver` with role-contract + queries. Retain a meaningful error for HT configurations missing required contribution support. + Do not impose new HT-only dependencies on legacy configurations. +- [x] Extract local-profile assembly from MIB operation and peer mutation. Preserve catalog/Tx/Rx + width intersection, antenna/stream limits, exact MCS bitmap, receiver short-GI information, + and existing MAC advertisement inputs such as A-MPDU exponent. +- [x] Preserve undefined, equal, and unequal Tx-MCS knowledge and conversion semantics. Equal Tx/Rx + support uses the exact bitmap; summary/unknown peer Tx knowledge must not become exact empty support. +- [x] Add management's operation-preparation procedure in the same change. It derives Basic HT-MCS, + width/secondary offset, protection configuration, and channel context from prepared inputs. Keep + existing parameter paths even when the procedure interpreting them moves out of the MIB. +- [x] Move configured-width rejection and band-dependent fallback decisions into management. Use PHY + band/mode legality APIs; preserve the distinction between an incapable configured PHY and a channel + requiring fallback. The MIB checks structural consistency and stores the result. +- [x] Supply explicit AP and ad hoc preparation paths. STA local capability preparation must not + invent an active BSS. Preserve initial radio channel information received before profile readiness. +- [x] Adapt existing callers while retaining initialization signaling temporarily. Keep this adapter + incapable of resetting BSS operation when local-profile preparation is called again. + +**Evidence:** extend `Ieee80211HtCapabilities_1.test` and `Ieee80211HtMgmtElements_1.test`; add a focused +module fixture using a replacement contributor that is not a subclass of the built-in concrete +Tx/Rx classes. Change its declared contribution and observe the assembled profile through the +production MAC path. Include local assembly with an existing active operation and verify no mutation. +Run the real built-in association fixture to compare emitted advertisements and antenna limits. + +**Exit:** capability installation cannot select a channel, create a BSS, change operation, or install +peers. Management supplies every removed operation-initialization effect. Built-in serialized +advertisements remain equivalent; replacement contributors work without assembler edits. + +## 6. P2 — commit complete BSS and relationship transitions + +**Primary files:** `Ieee80211Mib`, `Ieee80211MgmtBase`, `Ieee80211MgmtApBase`, +`Ieee80211MgmtAp`, `Ieee80211MgmtSta`, `Ieee80211MgmtStaSimplified`, +`Ieee80211MgmtApSimplified`, `Ieee80211MgmtAdhoc`, and `Ieee80211HtMgmtElements.h`. + +- [x] Introduce explicit local readiness, active BSS presence, band/channel context, and relationship + validity. Keep display/configuration identity separate where compatibility retains it after stop. +- [x] Introduce transition inputs that can be validated before mutation. Route relevant direct writes + to shared BSS/peer state through the owner-controlled commit path. Avoid exposing mutable references + that bypass invariants; preserve compatible inspection queries. +- [x] Implement the field-source and transition matrix below in detailed management. Keep discovery, + pending transactions, and active state separate. Make HT eligibility a validated result of the + relationship and applicable requirements, rather than an independently writable Boolean. +- [x] Commit AP relationships only for the matching acknowledged successful response. Evaluate current + local operation at commit, without modifying the transmitted response snapshot. Preserve AID + reservation/commit/cancel semantics and transaction matching. +- [x] Commit the STA's accepted successful response with response capability/operation fields plus + the selected discovery record's Basic HT-MCS requirement and decoding context. Preserve the + existing successful-association/legacy-fallback outcome for unusable HT information. +- [x] Route simplified management through equivalent local transition contracts. Resolve the AP + management endpoint through an appropriate typed preparation/transition contract; do not make the + STA an independent policy writer of AP internals. Preserve the explicit no-air abstraction. +- [x] Complete stop, crash, restart, peer replacement, and scoped transaction cleanup for all affected + roles. Restart uses retained prepared configuration and reestablishes operational state. +- [x] Preserve agreement cleanup through its existing algorithm owners. Equal advertisements must + not suppress same-address relationship replacement or required cleanup. +- [x] Adapt old selection inputs from the committed view until P3, with exactly one writer and a + documented removal point. No old mutable adapter becomes a second authority. + +### Required transition matrix + +| Stimulus | State action | Observable acceptance condition | +|---|---|---| +| Candidate Beacon/Probe Response | Update validated discovery only | Existing active relationship unchanged | +| Matching successful Association/Reassociation Response at STA | Merge accepted response fields with discovery Basic HT-MCS and channel context; commit relationship | Correct association outcome and independently classified HT usability | +| Matching successful response ACK at AP | Commit pending peer using current local operation | AP may have channel index 11 while STA retains response index 6 | +| Current-AP accepted Beacon, width only | Commit operation and reevaluate eligibility | Subsequent selection obeys width; P3 proves no capability rederivation | +| Current-AP accepted Beacon, HT absent or unsupported basic MCS | Preserve association; mark HT unusable through validated state | Existing bounded legacy fallback | +| Valid HT information returns | Reevaluate independently of historical capability equality | HT restored with previously seen capability bytes | +| Current-AP Probe Response | Discovery update | Existing discovery-only authoritative-state policy preserved | +| Malformed Beacon | Reject before authoritative mutation | No partial BSS/peer commit; existing rejection/timer behavior preserved | +| AP radio channel update | Compute and commit band, channel, operation together | Callback sees consistent channel interpretation | +| Failed reassociation to another AP | Clear affected pending state | Old active relationship retained | +| Failed reassociation to current AP | Apply existing same-AP teardown | Correct relationship and agreement cleanup | +| Timeout, refusal, queue drop, cancellation, late completion | Match transaction and clean its scope | No unrelated peer/active relationship changed | +| Relationship replacement, including same BSSID | Apply identity and cleanup rules before installation | No stale eligibility, agreements, or cache inherited | +| Stop/crash | Clear operational/peer state and pending resources | Operational queries inactive; simplified AP peer removed where resolvable | +| Restart | Reprepare operation/relationship from stable configuration | No stale state; local profile retained | + +### Publication contract + +Use a combined transition or explicit commit-then-publish API. Define the shared-state signal on the +MIB boundary and declare it in NED. Specify meaningful change detection, affected BSS/peer scope, +payload ownership, and borrowed lifetime. Management completes required AID/transaction/relationship +bookkeeping before publication and preserves existing protocol-notification completion points. + +In particular, fix the current ordering where `Ieee80211MgmtApBase::receiveSignal()` assigns +`radioBand` after calling the MIB channel setter. A synchronous notification must expose the new +band with its channel/operation, not the old band. A listener must never finish the transition. + +Choose and document a reentrancy policy: synchronous read-only observation is supported; nested +mutations must either be rejected explicitly or safely handled by the transition implementation. +Do not add generation counters solely to make publication convenient. Do not retain map references +across callbacks that could invalidate them. + +**Evidence:** extend the existing beacon, association, reassociation snapshot, AP/STA lifecycle, +deauthentication/disassociation, timeout, and queue-drop fixtures. Add an observer fixture that reads +identity, band, channel, operation, eligibility, and required transaction status synchronously. Vary +listener registration order and use two peers to prove scoped cleanup. Verify that an equal accepted +Beacon still refreshes applicable liveness/signal observations without a spurious operation event. + +**Exit:** all affected management writers use consistent transition paths; no observer can see a +half-committed relationship; legacy/no-BSS/unusable-HT states are distinguishable. Detailed, +simplified, and ad hoc lifecycle paths have explicit ownership. + +## 7. P3 — separate capability caching from operation checks + +**Primary files:** `mib/Ieee80211HtCapabilities.h`, `Ieee80211Mib`, +`mac/rateselection/Ieee80211PeerModeSelection.{h,cc}`, `RateSelection`, `QosRateSelection`, and +management consumers of negotiated results. + +- [x] Make directional derivation depend only on the prepared local profile and accepted peer + capability inputs. Remove the operation argument from the core derivation function. +- [x] Install/reuse the cache on semantic input equality. A changed capability input refreshes before + use; an operation-only change reevaluates eligibility without rebuilding directional support. +- [x] Keep capability equality separate from liveness, relationship identity, transaction completion, + and eligibility. Reinstallation after teardown builds fresh relationship state even at the same MAC. +- [x] Change the shared selector to receive a consistent explicit context: directional capabilities, + validated HT eligibility, and applicable local BSS operation. Preserve old frame-class and group + dispatch; do not introduce an association requirement for every management/control transmission. +- [x] Migrate both QoS and non-QoS production call sites together. Keep rate control responsible for + proposing/ranking modes; the helper remains a compatibility filter. +- [x] Preserve non-HT pass-through, exact sparse MCS support, directional receiver short GI, width + checks, proposed-bitrate ceiling, deterministic tie breaking, and bounded legacy fallback/errors. +- [x] Remove `Ieee80211NegotiatedHtCapabilities::operation` after the last reader migrates. Preserve + accepted local BSS state and immutable response/discovery snapshots. +- [x] Remove operation-triggered derivation loops and the unused peer `generation` field after a + fresh whole-tree reader check. Do not replace it with another public counter without a consumer. + +**Evidence:** extend `Ieee80211PeerModeSelection_1.test` for explicit contexts, missing/ineligible +state, sparse MCS, both short-GI directions, bitrate boundaries, and ties. Exercise both production +selection modules in module fixtures. Use test-only derivation instrumentation to establish: + +| Action on a live relationship | Expected derivation behavior | +|---|---| +| First usable capability installation | Derive once | +| Repeated equal advertisement and repeated selection | Reuse | +| Width/protection/basic-MCS-only update | Reuse; reevaluate applicable constraints | +| Changed capability inputs | Refresh before consumption | +| HT ineligibility then valid information returns | Restore correctly; reuse only if cached inputs remain valid | +| Teardown then same-address installation | Fresh relationship/cache | + +The 40-to-20 MHz fixture is synthetic evidence for management/selection; retain separate real +built-in 20 MHz packet-PHY association/serialization coverage. Do not describe that fixture as a +real 40 MHz PHY exchange. + +**Exit:** operation-only changes change selection where appropriate without capability rederivation; +no production reader uses a peer copy of current BSS operation; recovery works after both missing HT +and unsupported Basic HT-MCS updates. + +## 8. P4 — replace initialization broadcasts with declared dependencies + +**Primary files:** `Ieee80211Mac`, `mac/common/ModeSetListener.*`, all consumers below, their NED +declarations, management preparation code, both interface compositions, and +`src/inet/common/Simsignals.{h,cc}`. Remove the NED signal declaration and stale TODO references too. + +### Readiness design prerequisite + +The implementation must fill one worksheet row per consumer, recording **input, provider path, +input-ready stage, preparation stage/call, first read, and idempotency rule**. The following constrains +that design; it is not a claim that sibling callbacks at the same stage are ordered. + +| Prepared value | Earliest established prerequisite | Required preparation/first-use contract | +|---|---|---| +| Configured catalog | MAC `LOCAL` completed | Consumers query after `LOCAL`; no dependency on HT readiness | +| Local profile | Catalog plus configured PHY contributions/antenna inputs | Typed idempotent preparation after PHY readiness; complete before advertisement or peer derivation | +| Simplified identity | Interface addresses and AP SSID ready | Separate identity preparation complete before network configurators group interfaces | +| AP operation | Local profile and valid radio channel context | Typed idempotent AP preparation before advertisement or simplified HT installation | +| Simplified HT relationship | Both profiles plus prepared AP operation | Establish before its first initialization reader and before protocol operation | +| Timing/rate/policy state | Catalog and each consumer's local parameters/dependencies | Prepare once before the first protocol action; no reliance on broadcast order | + +Use the actual graph in `src/inet/common/InitStages.cc`: `LINK_LAYER` depends on `PHYSICAL_LAYER`, +`NETWORK_CONFIGURATION` depends on `LINK_LAYER`, and `NETWORK_INTERFACE_CONFIGURATION` declares +only a dependency on `LOCAL`. Do not assume address initialization precedes link-layer work merely +from current numeric ordering. Trace the address provider as part of the worksheet. + +The preferred design is post-`LOCAL` catalog queries and typed, idempotent preparation calls that can +prepare the peer's required inputs regardless of AP/STA declaration order. Such a call must only use +inputs already ready; it must not manually call another module's `initialize()` or lifecycle callback. +If a required address/PHY deadline cannot be met through existing declared dependencies, add the +smallest justified stage/dependency change and test it. Record that decision before removing the +signal; leaving the concrete stage/call mapping unresolved fails this milestone. + +### Starting consumer inventory + +Search for direct callbacks as well as classes inheriting `ModeSetListener`. Some consumers only use +the cached pointer later; others perform work in overridden callbacks. + +| Consumer group | Existing work/input to preserve | Migration task | +|---|---|---| +| `Dcaf`, `Edcaf` | Slot/SIFS/IFS/EIFS, CW defaults, access-category parameters | Invoke existing timing/contention preparation after querying catalog | +| `Dcf`, `Hcf` | Mode-dependent coordination behavior; signal forwarding | Replace catalog dependency while preserving unrelated signal handling | +| `RateSelection`, `QosRateSelection` | Catalog and fastest mandatory mode cache | Prepare query/cache explicitly; retain selection-history behavior | +| `RateControlBase` and concrete algorithms | Catalog and `resetRateControl()` initialization | Run setup once after concrete prerequisites; preserve separate lifecycle reset semantics | +| `Ieee80211MgmtBase` | Supported/Extended Supported Rates construction | Prepare before first frame construction; preserve bytes/basic-rate flags | +| `OriginatorAckPolicy`, `OriginatorQosAckPolicy`, `RtsPolicy`, `QosRtsPolicy` | Mode-dependent policy queries | Declare provider and establish readiness before use | +| `RecipientAckPolicy`, `RecipientQosAckPolicy`, `CtsPolicy`, `QosCtsPolicy` | Response timing/mode queries | Same explicit provider preparation | +| `TxopProcedure`, `SingleProtectionMechanism`, `OriginatorProtectionMechanism` | TXOP/protection mode inputs | Preserve preparation and subsequent timing behavior | +| `OriginatorBlockAckAgreementPolicy` | Catalog-dependent agreement policy | Explicit preparation without changing agreement ownership/lifetime | +| Other inherited/concurrent consumers found by P0 | Any additional callback or deferred catalog use | Add individual worksheet rows and focused checks | + +### Implementation tasks + +- [x] Add the MAC's typed catalog provider, such as a pure `getConfiguredModeSet()` query, in the + appropriate MAC contract package. NED `IIeee80211Mac` alone does not supply a C++ query interface. +- [x] Declare each consumer dependency using module parameters/contracts and connect defaults in + `Ieee80211Interface.ned` and `ExtUpperIeee80211Interface.ned`. Preserve `opMode`/`modeSet` forwarding + and allow explicitly configured standalone compositions. Do not cast to a containing-interface class. +- [x] Migrate consumer groups using the completed worksheet. Preserve callback side effects as + explicit preparation, not merely a stored pointer. Retain unrelated signals/subscriptions. +- [x] Preserve `numInitStages()` behavior when replacing the listener base class; check overrides and + initialization chains so later preparation stages still execute. +- [x] Split simplified identity preparation from HT relationship installation. Replace the `LAST` + retry with a deterministic preparation path. Ensure AP operation is ready in either declaration order. +- [x] Repeated preparation must not reset contention, rate estimates, agreements, or transaction state. + Stop/restart retains profiles and catalog while using the role-specific operational reset contract. +- [x] Remove signal emission only when all consumers have migrated. Remove listener implementation, + inheritance/includes, subscriptions, signal definition/declaration, NED metadata, and stale comments. +- [x] Recheck runtime radio setters/command handling and their callers. Preserve their compatibility; + do not reinterpret signal removal as either supporting or forbidding interface-wide reconfiguration. + +**Evidence:** update `Ieee80211MgmtStaSimplifiedInitialization_1.test` to preserve early identity +assertions and verify HT readiness at the new documented first-read boundary. Run AP-before-STA and +STA-before-AP orders, repeated preparation, stop/restart, missing-AP cleanup, prepared legacy state, +ad hoc, and standard/external-upper/standalone compositions. Compare timing, supported rates, and +initial rate-control state with P0. Test missing-provider diagnostics. + +**Exit:** no initialization reader depends on a sibling's or remote module's `LAST` publication; +no consumer relies on `modesetChanged`; all worksheet rows identify an implemented and tested path. + +## 9. Acceptance coverage and evidence ownership + +Existing test names below are starting points, not assertions of complete current coverage. New +fixtures should be added only where the existing production path cannot express the required probe. + +| Evidence ID | Claim and test layer | Existing starting point / required addition | Milestone | +|---|---|---|---| +| E1 | Exact directional capability derivation and representation, unit | `Ieee80211HtCapabilities_1.test`, `Ieee80211HtMgmtElements_1.test`; unknown/unequal/equal/sparse/short-GI cases | P1/P3 | +| E2 | Replaceable contributors and independent construction, module | New focused production-MAC contributor fixture; operation unchanged by assembly | P1 | +| E3 | Built-in advertised limits, module and serialization | `Ieee80211HtAssociation_1.test`, `Ieee80211HtAntennaRateControl_1.test`; real packet PHY remains 20 MHz | P1/P5 | +| E4 | Selection invariants, unit plus production integration | `Ieee80211PeerModeSelection_1.test`; QoS and non-QoS module checks | P3 | +| E5 | Width-only cache reuse, liveness, HT fallback/recovery, malformed input, module | `Ieee80211MgmtStaBeaconUpdate_1.test`; test-only derivation observation | P2/P3 | +| E6 | Response provenance and distinct AP/STA knowledge, module | `Ieee80211HtAssociation_1.test`, `Ieee80211MgmtApReassociationSnapshot_1.test`; response Basic HT-MCS merge | P2 | +| E7 | Transaction cleanup and lifecycle scope, module | AP/STA lifecycle, AP timeout/queue-drop/HCF variants, STA deauthentication/disassociation, agent reassociation tests | P2 | +| E8 | Atomic observer contract, module | New focused commit observer; listener-order and supported reentrancy probes | P2 | +| E9 | Initialization and composition, module/network | `Ieee80211MgmtStaSimplifiedInitialization_1.test`; both orders, external-upper, standalone, legacy/ad hoc fixtures | P4 | +| E10 | Channel context/fallback and generic-radio compatibility, module | `Ieee80211MgmtApChannelChange_1.test`, `Ieee80211MgmtApGenericRadio_1.test`, `Ieee80211MgmtApUnavailableChannel_1.test` | P1/P2 | +| E11 | Preserved peer exchange sequence, protocol | Inspect `tests/protocol/wifi/common/WifiAssociation.test` and `WifiReassociation.test`; add a focused case if the changed path is absent | P2/P5 | +| E12 | Unintended legacy trajectory changes, fingerprint | Explicitly selected WLAN legacy cases/configurations from the repository fingerprint catalog | P4/P5 | + +For each claim, record the production entry point and the assertion that detects a regression. A +helper-only test is not production-path evidence. Module tests prove state/owner contracts; +cross-peer sequence claims need protocol-category evidence even when an existing module fixture +provides useful complementary observations. If a fixture bypasses the PHY, label that boundary. + +Use deterministic fixtures with a pinned configuration, run 0, explicit seed, relevant parameters, +and a bounded duration. Enumerate both declaration orders, QoS modes, roles, and lifecycle outcomes +where required rather than hoping random seeds exercise them. A single seed suffices for an exact +state-transition reproduction; use a finite additional seed/configuration campaign only where timing +or randomized contention is part of the claim. Do not rerun unexplained failures until green. + +## 10. Verification commands and reporting + +Commands below are the original execution plan. Actual build/test results and final selectors are +recorded in the linked execution evidence. Both `inet_run_unit_tests` and `inet_run_module_tests` exist in the inspected checkout. +Run from the repository root with the normal INET/OMNeT++ environment active. + +After compiled source or generated-code inputs change, rebuild the library the tests load: + +```bash +make -j$(nproc) MODE=debug +inet_run_unit_tests -m debug -f 'Ieee80211(HtCapabilities_1|HtMgmtElements_1|HtModeSet_1|PeerModeSelection_1)\.test' +inet_run_module_tests -m debug -f 'Ieee80211(HtAssociation_1|HtAntennaRateControl_1|MgmtStaBeaconUpdate_1|MgmtApReassociationSnapshot_1|MgmtStaSimplifiedInitialization_1)\.test' +inet_run_module_tests -m debug -f 'Ieee80211(MgmtAp(Lifecycle|Timeout|QueueDrop|HcfQueueDrop|HcfRtsTimeout|ChannelChange|GenericRadio|UnavailableChannel|MalformedHtCap)|MgmtSta(Lifecycle|Deauthentication|Disassociation|Discovery)|AgentStaReassociation)_1\.test' +``` + +Use the appropriate subset during each milestone, and add explicit filters for every new fixture. +Resolve exact protocol invocation/build requirements from +[protocol AUTHORING](../../tests/protocol/lib/AUTHORING.md#10-running-tests) and the Wi-Fi suite's +current runner; the case filenames above are candidate coverage, not a fabricated runnable selector. +Resolve fingerprint cases/configurations against the actual catalog and record the final regex before +execution. A placeholder or a zero-case selection is `NOT_RUN`. + +Run focused architecture checks for the changed paths: + +```bash +doc/project/enforcement/check-architecture.sh src/inet/linklayer/ieee80211 +doc/project/enforcement/check-architecture.sh src/inet/physicallayer/wireless/ieee80211 +doc/project/enforcement/check-architecture.sh src/inet/emulation/linklayer/ieee80211 +doc/project/enforcement/check-interfaces.sh +``` + +For final integration, follow the current [run-the-gates guide](../../doc/project/guide/run-the-gates.md): +fresh debug and release compilation, focused tests, project-wide architecture/naming/interface/seal +gates, commit/classification gates against the actual PR base, and the general/WLAN semantic +checklists. Include changed common signal/stage code in the gate scope. Broad C++ changes also need +the documented C++ checker with a compilation database. Distinguish pre-existing gate findings from +new defects and reconcile them with the ledgers rather than hiding them. + +Store an evidence manifest with one row per command: milestone, tested revision, working directory, +exact command/filter, build mode, configuration/run/seed, selected/executed count, exit status, +outcome classification, and log/artifact paths. Keep helper, module, protocol, and fingerprint claims +separate. Setup failures, expected failures, and missing coverage are not passes. + +If a fingerprint changes, diagnose the first relevant divergence against the pinned baseline. An +intentional stage move still needs a behavioral explanation and focused proof. Changes to recorded +baselines follow [change-a-baseline](../../doc/project/guide/change-a-baseline.md); do not regenerate +fingerprints simply to make this refactor pass. + +## 11. P5 — completion audit + +- [x] Every P0 reader/writer is migrated or explicitly justified as a historical transaction/discovery + snapshot. No remaining production writer bypasses the relevant transition invariant. +- [x] Search confirms no concrete built-in Tx/Rx cast in capability assembly, no obsolete initialization + signal/listener path, and no peer copy of current operation. Review legitimate similarly named PHY + catalog/runtime-command APIs instead of deleting them mechanically. +- [x] No operation-only path rebuilds capability intersections. No selection path rebuilds them on + each frame. Equality cannot suppress liveness, eligibility restoration, or relationship replacement. +- [x] No new public counter exists solely for tests. WATCH/display inspection reflects authoritative + state and does not imply active operation from retained display identity. +- [x] Both selection paths and every affected management role meet the acceptance matrix. The + AP-current/STA-accepted channel distinction and response Basic HT-MCS provenance remain explicit. +- [x] The preparation worksheet is complete, source-backed, and exercised in both declaration orders. + All required preparation finishes before first use without cross-module `LAST` dependence. +- [x] New provider/dependency contracts work in standard, external-upper, and explicit standalone + compositions, including legacy behavior and useful missing-dependency diagnostics. +- [x] All migration adapters listed in section 2 are removed. Stable parameters and radio command + semantics remain compatible; deferred feature work has not entered the patch series. +- [x] Final focused evidence, fresh debug/release build results, gate outcomes, and unresolved gaps are + recorded. A required behavioral coverage gap prevents marking the implementation complete. +- [x] Review the final change using the canonical code/PR guides and general/WLAN checklists. Update + canonical contract documentation only where the implementation adds concrete details; link this plan + rather than duplicating project policy. Move the plan to `plan/done/` only after its work is complete. + +Completion means the new contracts govern every changed production path and the focused evidence +supports the preserved behavior. This planning document does not authorize unrelated feature work, +baseline replacement, or modification of sealed source. diff --git a/src/inet/common/Simsignals.cc b/src/inet/common/Simsignals.cc index 126e6b6ea7e..e79367a3d83 100644 --- a/src/inet/common/Simsignals.cc +++ b/src/inet/common/Simsignals.cc @@ -21,7 +21,6 @@ simsignal_t l2ApDisassociatedSignal = cComponent::registerSignal("l2ApDisassocia simsignal_t linkBrokenSignal = cComponent::registerSignal("linkBroken"); -simsignal_t modesetChangedSignal = cComponent::registerSignal("modesetChanged"); simsignal_t interpacketGapStartedSignal = cComponent::registerSignal("interpacketGapStarted"); simsignal_t interpacketGapEndedSignal = cComponent::registerSignal("interpacketGapEnded"); diff --git a/src/inet/common/Simsignals.h b/src/inet/common/Simsignals.h index 69a1363fe52..3301001fe46 100644 --- a/src/inet/common/Simsignals.h +++ b/src/inet/common/Simsignals.h @@ -30,7 +30,6 @@ extern INET_API simsignal_t // admin linkBrokenSignal, // used for manet link layer feedback - modesetChangedSignal, interpacketGapStartedSignal, interpacketGapEndedSignal, diff --git a/src/inet/linklayer/ieee80211/__TODO b/src/inet/linklayer/ieee80211/__TODO index 6bf30247628..2053bf77fe1 100644 --- a/src/inet/linklayer/ieee80211/__TODO +++ b/src/inet/linklayer/ieee80211/__TODO @@ -14,7 +14,6 @@ @signal[packetSentToPeer](type=inet::Packet); @signal[packetDropped](type=inet::Packet); @signal[linkBroken](type=inet::Packet); - @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); @statistic[packetDropIncorrectlyReceived](record=count); @statistic[packetDropNotAddressedToUs](record=count); @statistic[packetDropQueueOverflow](record=count); diff --git a/src/inet/linklayer/ieee80211/mac/Ds.cc b/src/inet/linklayer/ieee80211/mac/Ds.cc index ff26ccba776..fe93a5f219e 100644 --- a/src/inet/linklayer/ieee80211/mac/Ds.cc +++ b/src/inet/linklayer/ieee80211/mac/Ds.cc @@ -31,7 +31,7 @@ void Ds::processDataFrame(Packet *frame, const Ptr& h if (mib->mode == Ieee80211Mib::INDEPENDENT) mac->sendUp(frame); else if (mib->mode == Ieee80211Mib::INFRASTRUCTURE) { - if (mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) { + if (mib->getBssStationData().stationType == Ieee80211Mib::ACCESS_POINT) { // check toDS bit if (!header->getToDS()) { // looks like this is not for us - discard @@ -50,8 +50,8 @@ void Ds::processDataFrame(Packet *frame, const Ptr& h return; } // look up destination address in our STA list - auto it = mib->bssAccessPointData.stations.find(header->getAddress3()); - if (it == mib->bssAccessPointData.stations.end()) { + auto it = mib->getBssAccessPointData().stations.find(header->getAddress3()); + if (it == mib->getBssAccessPointData().stations.end()) { EV_WARN << "Frame's destination address is not in our STA list -- passing up\n"; mac->sendUp(frame); } @@ -70,15 +70,15 @@ void Ds::processDataFrame(Packet *frame, const Ptr& h } } } - else if (mib->bssStationData.stationType == Ieee80211Mib::STATION) { - if (!mib->bssStationData.isAssociated) { + else if (mib->getBssStationData().stationType == Ieee80211Mib::STATION) { + if (!mib->getBssStationData().isAssociated) { EV_WARN << "Rejecting data frame as STA is not associated with an AP" << endl; PacketDropDetails details; details.setReason(OTHER_PACKET_DROP); emit(packetDroppedSignal, frame, &details); delete frame; } - else if (mib->bssData.bssid != header->getTransmitterAddress()) { + else if (mib->getBssData().bssid != header->getTransmitterAddress()) { EV_WARN << "Rejecting data frame received from another AP" << endl; PacketDropDetails details; details.setReason(OTHER_PACKET_DROP); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index d87f581231f..da8dd51225e 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -27,9 +27,9 @@ #include "inet/linklayer/ieee80211/mac/contract/ITx.h" #include "inet/networklayer/contract/IInterfaceTable.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" -#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" -#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.h" namespace inet { namespace ieee80211 { @@ -71,21 +71,7 @@ void Ieee80211Mac::initialize(int stage) ds = check_and_cast(getSubmodule("ds")); rx = check_and_cast(getSubmodule("rx")); tx = check_and_cast(getSubmodule("tx")); - int operationalHtSpatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), - modeSet->getMaximumNumberOfSpatialStreams()); - std::set operationalChannelWidths; - if (modeSet->isHtOperationSupported()) { - const auto *transmitter = dynamic_cast(radio->getTransmitter()); - const auto *receiver = dynamic_cast(radio->getReceiver()); - if (transmitter == nullptr || receiver == nullptr) - throw cRuntimeError("HT operation requires Ieee80211Transmitter and Ieee80211Receiver"); - for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) - if (transmitter->isHtChannelWidthSupported(channelWidth) && - receiver->isHtChannelWidthSupported(channelWidth)) - operationalChannelWidths.insert(channelWidth); - } - mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); - emit(modesetChangedSignal, modeSet); + prepareLocalCapabilities(); if (isUp()) initializeRadioMode(); rx = check_and_cast(getSubmodule("rx")); @@ -97,6 +83,68 @@ void Ieee80211Mac::initialize(int stage) } } +void Ieee80211Mac::prepareLocalCapabilities() +{ + Enter_Method("prepareLocalCapabilities"); + if (mib->hasPreparedLocalCapabilities()) + return; + if (!modeSet->isHtOperationSupported()) { + mib->installLocalHtCapabilities(Ieee80211HtCapabilities(), false); + return; + } + auto *configuredRadio = check_and_cast(gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + const auto *transmitter = dynamic_cast(configuredRadio->getTransmitter()); + const auto *receiver = dynamic_cast(configuredRadio->getReceiver()); + if (transmitter == nullptr || receiver == nullptr) + throw cRuntimeError("HT operation requires transmitter and receiver capability providers"); + int operationalHtSpatialStreamLimit = std::min(configuredRadio->getAntenna()->getNumAntennas(), + modeSet->getMaximumNumberOfSpatialStreams()); + std::set operationalChannelWidths; + for (auto width : modeSet->getHtSupportedChannelWidths()) + if (transmitter->isHtChannelWidthSupported(width) && receiver->isHtChannelWidthSupported(width)) + operationalChannelWidths.insert(width); + Ieee80211HtCapabilities localHtCapabilities; + if (operationalHtSpatialStreamLimit <= 0) + throw cRuntimeError("HT operation requires a positive operational spatial-stream limit"); + + // IEEE Std 802.11-2024, 9.4.2.54.4 and 9.4.2.55: advertise exactly the + // HT modes from the authoritative mode set, while advertised channel + // widths are restricted to those the configured transmitter and receiver + // can actually operate. In particular, do not infer dense MCS blocks or HT + // widths from legacy/VHT modes that happen to share the set. + for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) + if (operationalChannelWidths.count(channelWidth) != 0) + localHtCapabilities.supportedChannelWidths.insert(channelWidth); + localHtCapabilities.shortGi20 = localHtCapabilities.supportedChannelWidths.count(MHz(20)) != 0 && + modeSet->isHtShortGuardIntervalSupported(MHz(20)) && receiver->isHtShortGuardIntervalSupported(MHz(20)); + localHtCapabilities.shortGi40 = localHtCapabilities.supportedChannelWidths.count(MHz(40)) != 0 && + modeSet->isHtShortGuardIntervalSupported(MHz(40)) && receiver->isHtShortGuardIntervalSupported(MHz(40)); + for (int index = 0; index < modeSet->getNumModes(); index++) { + const auto *mode = modeSet->getMode(index); + int mcs = mode->getHtMcsIndex(); + if (mcs >= 0 && mcs < 77 && operationalChannelWidths.count(mode->getDataMode()->getBandwidth()) != 0 && + mode->getDataMode()->getNumberOfSpatialStreams() <= operationalHtSpatialStreamLimit) + localHtCapabilities.rxMcsSupported[mcs] = true; + } + // The equal-case Tx MCS set is represented by the maximum MCS index per + // spatial-stream group. Rebuild it from the filtered Rx bitmap; MCS 32 is + // not part of this map's MCS 0..31 NSS encoding. + localHtCapabilities.txMcsNss = Ieee80211HtMcsNssMap(); + for (int mcs = 0; mcs < 32; mcs++) { + if (localHtCapabilities.rxMcsSupported[mcs]) { + int nss = mcs / 8; + localHtCapabilities.txMcsNss.maxMcsPerNss[nss] = std::max(localHtCapabilities.txMcsNss.maxMcsPerNss[nss], mcs % 8); + } + } + if (localHtCapabilities.supportedChannelWidths.empty()) + throw cRuntimeError("HT operation mode set '%s' does not provide an HT channel width", modeSet->getName()); + localHtCapabilities.maxAmpduLengthExponent = mib->par("htMaxAmpduLengthExponent"); + if (localHtCapabilities.maxAmpduLengthExponent < 0 || localHtCapabilities.maxAmpduLengthExponent > 3) + throw cRuntimeError("htMaxAmpduLengthExponent must be between 0 and 3"); + + mib->installLocalHtCapabilities(localHtCapabilities, true); +} + void Ieee80211Mac::initializeRadioMode() { const char *initialRadioMode = par("initialRadioMode"); @@ -164,8 +212,8 @@ void Ieee80211Mac::handleMgmtPacket(Packet *packet) const auto& header = makeShared(); header->setType((Ieee80211FrameType)packet->getTag()->getSubtype()); header->setReceiverAddress(packet->getTag()->getDestAddress()); - if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) - header->setAddress3(mib->bssData.bssid); + if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->getBssStationData().stationType == Ieee80211Mib::ACCESS_POINT) + header->setAddress3(mib->getBssData().bssid); packet->insertAtFront(header); packet->insertAtBack(makeShared()); processUpperFrame(packet, header); @@ -173,7 +221,7 @@ void Ieee80211Mac::handleMgmtPacket(Packet *packet) void Ieee80211Mac::handleUpperPacket(Packet *packet) { - if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->bssStationData.stationType == Ieee80211Mib::STATION && !mib->bssStationData.isAssociated) { + if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->getBssStationData().stationType == Ieee80211Mib::STATION && !mib->getBssStationData().isAssociated) { EV << "STA is not associated with an access point, discarding packet " << packet << "\n"; PacketDropDetails details; details.setReason(OTHER_PACKET_DROP); @@ -183,11 +231,11 @@ void Ieee80211Mac::handleUpperPacket(Packet *packet) } encapsulate(packet); const auto& header = packet->peekAtFront(); - if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) { + if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->getBssStationData().stationType == Ieee80211Mib::ACCESS_POINT) { auto receiverAddress = header->getReceiverAddress(); if (!receiverAddress.isMulticast()) { - auto it = mib->bssAccessPointData.stations.find(receiverAddress); - if (it == mib->bssAccessPointData.stations.end() || it->second != Ieee80211Mib::ASSOCIATED) { + auto it = mib->getBssAccessPointData().stations.find(receiverAddress); + if (it == mib->getBssAccessPointData().stations.end() || it->second != Ieee80211Mib::ASSOCIATED) { EV << "STA with MAC address " << receiverAddress << " not associated with this AP, dropping frame\n"; PacketDropDetails details; details.setReason(OTHER_PACKET_DROP); @@ -259,14 +307,14 @@ void Ieee80211Mac::encapsulate(Packet *packet) if (mib->mode == Ieee80211Mib::INDEPENDENT) header->setReceiverAddress(destAddress); else if (mib->mode == Ieee80211Mib::INFRASTRUCTURE) { - if (mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) { + if (mib->getBssStationData().stationType == Ieee80211Mib::ACCESS_POINT) { header->setFromDS(true); header->setAddress3(mib->address); header->setReceiverAddress(destAddress); } - else if (mib->bssStationData.stationType == Ieee80211Mib::STATION) { + else if (mib->getBssStationData().stationType == Ieee80211Mib::STATION) { header->setToDS(true); - header->setReceiverAddress(mib->bssData.bssid); + header->setReceiverAddress(mib->getBssData().bssid); header->setAddress3(destAddress); } else @@ -300,11 +348,11 @@ void Ieee80211Mac::decapsulate(Packet *packet) macAddressInd->setDestAddress(header->getReceiverAddress()); } else if (mib->mode == Ieee80211Mib::INFRASTRUCTURE) { - if (mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) { + if (mib->getBssStationData().stationType == Ieee80211Mib::ACCESS_POINT) { macAddressInd->setSrcAddress(header->getTransmitterAddress()); macAddressInd->setDestAddress(header->getAddress3()); } - else if (mib->bssStationData.stationType == Ieee80211Mib::STATION) { + else if (mib->getBssStationData().stationType == Ieee80211Mib::STATION) { macAddressInd->setSrcAddress(header->getAddress3()); macAddressInd->setDestAddress(header->getReceiverAddress()); } diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 1114deaed00..09b69439f3a 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -11,6 +11,7 @@ #include "inet/common/ModuleRefByPar.h" #include "inet/linklayer/base/MacProtocolBase.h" #include "inet/linklayer/ieee80211/mac/contract/IDs.h" +#include "inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRx.h" @@ -35,7 +36,7 @@ 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 IIeee80211MacConfiguration { public: static simsignal_t frameTransmissionOutcomeSignal; @@ -100,6 +101,9 @@ class INET_API Ieee80211Mac : public MacProtocolBase Ieee80211Mac(); virtual ~Ieee80211Mac(); + void prepareLocalCapabilities() override; + const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const override { return modeSet; } + virtual FcsMode getFcsMode() const { return fcsMode; } virtual const MacAddress& getAddress() const { return mib->address; } virtual void sendUp(cMessage *message) override; diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned index 51f4198660a..bd2f9b93915 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned @@ -7,6 +7,8 @@ package inet.linklayer.ieee80211.mac; +import inet.linklayer.ieee80211.mac.contract.IIeee80211MacConfiguration; + import inet.linklayer.base.MacProtocolBase; import inet.linklayer.ieee80211.IIeee80211Mac; import inet.linklayer.ieee80211.mac.contract.IDcf; @@ -74,7 +76,7 @@ import inet.linklayer.ieee80211.mac.contract.ITx; // which is related to power management, capability information // which is related to PCF and other non-modeled features). // -module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac +module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac, IIeee80211MacConfiguration { parameters: string mibModule; @@ -85,6 +87,7 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac int mtu @unit(B) = default(2304B); bool qosStation = default(false); + **.modeSetModule = default(absPath(".")); *.mibModule = default(absPath(this.mibModule)); *.rxModule = "^.rx"; *.txModule = "^.tx"; @@ -92,7 +95,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/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 44b4c0874fa..5a40aeb40cc 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -17,7 +17,7 @@ Define_Module(OriginatorBlockAckAgreementPolicy); void OriginatorBlockAckAgreementPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { ackPolicy = check_and_cast(getModuleByPath(par("originatorAckPolicyModule"))); delayedAckPolicySupported = par("delayedAckPolicySupported"); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index b762cc29828..88fadd48377 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -9,13 +9,13 @@ #define __INET_ORIGINATORBLOCKACKAGREEMENTPOLICY_H #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h" namespace inet { namespace ieee80211 { -class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, public IOriginatorBlockAckAgreementPolicy +class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetModuleBase, public IOriginatorBlockAckAgreementPolicy { protected: IOriginatorQoSAckPolicy *ackPolicy = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index bbb569b4b20..68ee746d154 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IOriginatorBlockAckAgreementPolicy; simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBlockAckAgreementPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(OriginatorBlockAckAgreementPolicy); string originatorAckPolicyModule; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 1f6ea0615cb..71b157c6e77 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -21,11 +21,8 @@ Define_Module(Dcaf); void Dcaf::initialize(int stage) { - if (stage == INITSTAGE_LOCAL) { - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); - } - else if (stage == INITSTAGE_LINK_LAYER) { - // TODO calculateTimingParameters() + ModeSetModuleBase::initialize(stage); + if (stage == INITSTAGE_LINK_LAYER) { pendingQueue = check_and_cast(getSubmodule("pendingQueue")); inProgressFrames = check_and_cast(getSubmodule("inProgressFrames")); contention = check_and_cast(getSubmodule("contention")); @@ -122,15 +119,6 @@ void Dcaf::expectedChannelAccess(simtime_t time) // don't care } -void Dcaf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) -{ - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } -} } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h index 2fab0d312c8..29fa80fb55e 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h @@ -8,7 +8,7 @@ #ifndef __INET_DCAF_H #define __INET_DCAF_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IChannelAccess.h" #include "inet/linklayer/ieee80211/mac/contract/IContention.h" #include "inet/linklayer/ieee80211/mac/contract/IRecoveryProcedure.h" @@ -17,10 +17,9 @@ namespace inet { namespace ieee80211 { -class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener +class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetModuleBase { protected: - physicallayer::Ieee80211ModeSet *modeSet = nullptr; IContention *contention = nullptr; IChannelAccess::ICallback *callback = nullptr; @@ -43,7 +42,6 @@ class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, publ virtual void initialize(int stage) override; virtual void calculateTimingParameters(); - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; public: virtual queueing::IPacketQueue *getPendingQueue() const { return pendingQueue; } diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.ned b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.ned index f6749e32cca..10a0aa43c39 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.ned +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.ned @@ -18,6 +18,7 @@ import inet.linklayer.ieee80211.mac.queue.InProgressFrames; module Dcaf extends Module { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider int difsn = default(-1); int cwMin = default(-1); int cwMax = default(-1); diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index e220635a876..b0070f3c401 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -28,8 +28,8 @@ Edcaf::~Edcaf() void Edcaf::initialize(int stage) { + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); ac = getAccessCategory(par("accessCategory")); contention = check_and_cast(getSubmodule("contention")); collisionController = check_and_cast(getModuleByPath(par("collisionControllerModule"))); @@ -186,15 +186,6 @@ 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) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } -} } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index e737cad9162..4e984fb03a0 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h @@ -10,7 +10,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/common/AccessCategory.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/common/StationRetryCounters.h" #include "inet/linklayer/ieee80211/mac/contract/IChannelAccess.h" #include "inet/linklayer/ieee80211/mac/contract/IContention.h" @@ -28,7 +28,7 @@ namespace ieee80211 { /** * Implements IEEE 802.11 Enhanced Distributed Channel Access Function. */ -class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener +class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetModuleBase { protected: IContention *contention = nullptr; @@ -61,7 +61,6 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub protected: 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 AccessCategory getAccessCategory(const char *ac); virtual int getAifsNumber(AccessCategory ac); diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.ned b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.ned index edacd445fe3..b67dadf2cd2 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.ned +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.ned @@ -22,6 +22,7 @@ import inet.linklayer.ieee80211.mac.queue.InProgressFrames; module Edcaf extends Module { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider string rxModule; string collisionControllerModule; string originatorMacDataServiceModule; diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc deleted file mode 100644 index 15dd3838eb5..00000000000 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright (C) 2016 OpenSim Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later -// - - -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" - -#include "inet/common/ModuleAccess.h" -#include "inet/common/Simsignals.h" -#include "inet/networklayer/common/NetworkInterface.h" - -namespace inet { -namespace ieee80211 { - -void ModeSetListener::initialize(int stage) -{ - if (stage == INITSTAGE_LOCAL) - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); -} - -void ModeSetListener::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) -{ - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) - modeSet = check_and_cast(obj); -} - -} /* namespace ieee80211 */ -} /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h deleted file mode 100644 index 05581613050..00000000000 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright (C) 2016 OpenSim Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later -// - - -#ifndef __INET_MODESETLISTENER_H -#define __INET_MODESETLISTENER_H - -#include "inet/common/SimpleModule.h" -#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" - -namespace inet { -namespace ieee80211 { - -class INET_API ModeSetListener : public SimpleModule, public cListener -{ - protected: - physicallayer::Ieee80211ModeSet *modeSet = nullptr; - - protected: - 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; -}; - -} /* namespace ieee80211 */ -} /* namespace inet */ - -#endif - diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc new file mode 100644 index 00000000000..77406a7179e --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc @@ -0,0 +1,20 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" +namespace inet::ieee80211 { +void ModeSetModuleBase::initialize(int stage) +{ + if (stage == INITSTAGE_LOCAL) + modeSetProvider.reference(this, "modeSetModule", true); + else if (stage == INITSTAGE_LINK_LAYER) { + modeSet = modeSetProvider->getConfiguredModeSet(); + if (modeSet == nullptr) + throw cRuntimeError("Configured IEEE 802.11 mode catalog is unavailable"); + } +} +} // namespace inet::ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h new file mode 100644 index 00000000000..2092283634e --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h @@ -0,0 +1,26 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef INET_MODESETMODULEBASE_H +#define INET_MODESETMODULEBASE_H + +#include "inet/common/SimpleModule.h" +#include "inet/common/ModuleRefByPar.h" +#include "inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +namespace inet::ieee80211 { +/** Base for modules with a declared, configuration-lifetime catalog dependency. */ +class INET_API ModeSetModuleBase : public SimpleModule +{ + protected: + ModuleRefByPar modeSetProvider; + const physicallayer::Ieee80211ModeSet *modeSet = nullptr; + int numInitStages() const override { return NUM_INIT_STAGES; } + void initialize(int stage) override; +}; +} // namespace inet::ieee80211 +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h new file mode 100644 index 00000000000..27b67801365 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h @@ -0,0 +1,27 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef INET_IIEEE80211MACCONFIGURATION_H +#define INET_IIEEE80211MACCONFIGURATION_H + +#include "inet/common/INETDefs.h" + +namespace inet::physicallayer { class Ieee80211ModeSet; } + +namespace inet::ieee80211 { +/** Configured catalog is ready after LOCAL; explicit preparation requires PHY readiness. + * Repeated preparation preserves protocol and algorithm state. + */ +class INET_API IIeee80211MacConfiguration +{ + public: + virtual ~IIeee80211MacConfiguration() = default; + [[nodiscard]] virtual const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const = 0; + virtual void prepareLocalCapabilities() = 0; +}; +} // namespace inet::ieee80211 +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned new file mode 100644 index 00000000000..ed517dddcee --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned @@ -0,0 +1,12 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee80211.mac.contract; +// Configured mode catalog and explicit capability preparation; see the C++ contract. +moduleinterface IIeee80211MacConfiguration +{ +} diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index ddb1cf9aa65..e286df0df07 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -25,7 +25,7 @@ Define_Module(Dcf); void Dcf::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LINK_LAYER) { startRxTimer = new cMessage("startRxTimeout"); mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); @@ -132,8 +132,6 @@ void Dcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); } } - else - ModeSetListener::receiveSignal(source, signalID, obj, details); } void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 51ce7172954..21908e37454 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -9,7 +9,7 @@ #define __INET_DCF_H #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" #include "inet/linklayer/ieee80211/mac/contract/ICtsPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/ICtsProcedure.h" @@ -37,7 +37,7 @@ class Ieee80211Mac; /** * Implements IEEE 802.11 Distributed Coordination Function. */ -class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public ModeSetListener +class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public ModeSetModuleBase, public cListener { protected: Ieee80211Mac *mac = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned index df457a9149d..66364ef7a4e 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.ned @@ -28,6 +28,7 @@ import inet.linklayer.ieee80211.mac.recipient.RecipientMacDataService; module Dcf extends Module like IDcf { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider string rxModule; string txModule; string mibModule = default("^.^.mib"); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index e64ce4db852..f4473f4b311 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -33,7 +33,7 @@ Define_Module(Hcf); void Hcf::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); startRxTimer = new cMessage("startRxTimeout"); @@ -113,7 +113,7 @@ void Hcf::handleMessage(cMessage *msg) void Hcf::refreshDisplay() const { - ModeSetListener::refreshDisplay(); + ModeSetModuleBase::refreshDisplay(); if (frameSequenceHandler->isSequenceRunning()) { auto history = frameSequenceHandler->getFrameSequence()->getHistory(); getDisplayString().setTagArg("tt", 0, ("Fs: " + history).c_str()); @@ -165,8 +165,6 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); } } - else - ModeSetListener::receiveSignal(source, signalID, obj, details); } void Hcf::scheduleStartRxTimer(simtime_t timeout) diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 72f7af70fc4..22398f73831 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -10,7 +10,7 @@ #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IAckHandler.h" #include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" @@ -45,7 +45,7 @@ class Ieee80211Mac; /** * Implements IEEE 802.11 Hybrid Coordination Function. */ -class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public IBlockAckAgreementHandlerCallback, public ModeSetListener +class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public IBlockAckAgreementHandlerCallback, public ModeSetModuleBase, public cListener { public: static simsignal_t edcaCollisionDetectedSignal; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 31b8bfc7047..cac8079b706 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -28,6 +28,7 @@ import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; module Hcf extends Module like IHcf { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider bool isBlockAckSupported = default(false); string rxModule; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc index ae6ab2207c1..b0d8c41efd5 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc @@ -12,7 +12,7 @@ namespace ieee80211 { using namespace inet::physicallayer; -FrameSequenceContext::FrameSequenceContext(MacAddress address, Ieee80211ModeSet *modeSet, InProgressFrames *inProgressFrames, IRtsProcedure *rtsProcedure, IRtsPolicy *rtsPolicy, NonQoSContext *nonQoSContext, QoSContext *qosContext) : +FrameSequenceContext::FrameSequenceContext(MacAddress address, const Ieee80211ModeSet *modeSet, InProgressFrames *inProgressFrames, IRtsProcedure *rtsProcedure, IRtsPolicy *rtsPolicy, NonQoSContext *nonQoSContext, QoSContext *qosContext) : address(address), modeSet(modeSet), inProgressFrames(inProgressFrames), diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h index bf2370a6b66..f6a200c0547 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h @@ -55,7 +55,7 @@ class INET_API FrameSequenceContext : public cObject protected: simtime_t startTime = simTime(); MacAddress address = MacAddress::UNSPECIFIED_ADDRESS; - physicallayer::Ieee80211ModeSet *modeSet = nullptr; + const physicallayer::Ieee80211ModeSet *modeSet = nullptr; InProgressFrames *inProgressFrames = nullptr; std::vector steps; @@ -66,7 +66,7 @@ class INET_API FrameSequenceContext : public cObject QoSContext *qosContext = nullptr; public: - FrameSequenceContext(MacAddress address, physicallayer::Ieee80211ModeSet *modeSet, InProgressFrames *inProgressFrames, IRtsProcedure *rtsProcedure, IRtsPolicy *rtsPolicy, NonQoSContext *nonQosContext, QoSContext *qosContext); + FrameSequenceContext(MacAddress address, const physicallayer::Ieee80211ModeSet *modeSet, InProgressFrames *inProgressFrames, IRtsProcedure *rtsProcedure, IRtsPolicy *rtsPolicy, NonQoSContext *nonQosContext, QoSContext *qosContext); virtual ~FrameSequenceContext(); virtual simtime_t getDuration() const { return simTime() - startTime; } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.cc index 699235a2000..12972a8076d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.cc @@ -14,7 +14,7 @@ Define_Module(OriginatorAckPolicy); void OriginatorAckPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); ackTimeout = par("ackTimeout"); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.h index aa6d6528b56..250617c602f 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.h @@ -8,14 +8,14 @@ #ifndef __INET_ORIGINATORACKPOLICY_H #define __INET_ORIGINATORACKPOLICY_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorAckPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" namespace inet { namespace ieee80211 { -class INET_API OriginatorAckPolicy : public ModeSetListener, public IOriginatorAckPolicy +class INET_API OriginatorAckPolicy : public ModeSetModuleBase, public IOriginatorAckPolicy { protected: IRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.ned index 49ed1be3d62..3e0dbb05577 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorAckPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IOriginatorAckPolicy; simple OriginatorAckPolicy extends SimpleModule like IOriginatorAckPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(OriginatorAckPolicy); string rateSelectionModule; double ackTimeout @unit(s) = default(-1s); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 90cc7602199..206d9e8b73d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -16,7 +16,7 @@ Define_Module(OriginatorQosAckPolicy); void OriginatorQosAckPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); maxBlockAckPolicyFrameLength = par("maxBlockAckPolicyFrameLength"); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index d25da018cdc..5e1b33914ab 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -9,14 +9,14 @@ #define __INET_ORIGINATORQOSACKPOLICY_H #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h" namespace inet { namespace ieee80211 { -class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginatorQoSAckPolicy +class INET_API OriginatorQosAckPolicy : public ModeSetModuleBase, public IOriginatorQoSAckPolicy { protected: IQosRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned index eb2f9236ab1..953c8901008 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IOriginatorQosAckPolicy; simple OriginatorQosAckPolicy extends SimpleModule like IOriginatorQosAckPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(OriginatorQosAckPolicy); string rateSelectionModule; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.cc index fa1e42f77c8..88591dc0be7 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.cc @@ -14,7 +14,7 @@ Define_Module(QosRtsPolicy); void QosRtsPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rtsThreshold = par("rtsThreshold"); ctsTimeout = par("ctsTimeout"); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.h index ac5c59840bf..ae19897b45b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.h @@ -8,14 +8,14 @@ #ifndef __INET_QOSRTSPOLICY_H #define __INET_QOSRTSPOLICY_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRtsPolicy.h" namespace inet { namespace ieee80211 { -class INET_API QosRtsPolicy : public ModeSetListener, public IRtsPolicy +class INET_API QosRtsPolicy : public ModeSetModuleBase, public IRtsPolicy { protected: IQosRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.ned index d19f5b55260..9d0625cc905 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IRtsPolicy; simple QosRtsPolicy extends SimpleModule like IRtsPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(QosRtsPolicy); string rateSelectionModule; double ctsTimeout @unit(s) = default(-1s); diff --git a/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.cc index fc75816f253..d2290f93dac 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.cc @@ -14,7 +14,7 @@ Define_Module(RtsPolicy); void RtsPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); rtsThreshold = par("rtsThreshold"); diff --git a/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.h index f8a8e042de5..af78eeca467 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.h @@ -8,14 +8,14 @@ #ifndef __INET_RTSPOLICY_H #define __INET_RTSPOLICY_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRtsPolicy.h" namespace inet { namespace ieee80211 { -class INET_API RtsPolicy : public ModeSetListener, public IRtsPolicy +class INET_API RtsPolicy : public ModeSetModuleBase, public IRtsPolicy { protected: int rtsThreshold = -1; diff --git a/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.ned index 331f0739d52..e963cca9d65 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/RtsPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IRtsPolicy; simple RtsPolicy extends SimpleModule like IRtsPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(RtsPolicy); string rateSelectionModule; double ctsTimeout @unit(s) = default(-1s); diff --git a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc index 9b7e6184293..1b15a6ee6b2 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc @@ -20,7 +20,7 @@ Define_Module(TxopProcedure); void TxopProcedure::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { limit = par("txopLimit"); WATCH(start); diff --git a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h index 656b74e8055..820dd6dea8b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h @@ -9,14 +9,14 @@ #define __INET_TXOPPROCEDURE_H #include "inet/linklayer/ieee80211/mac/common/AccessCategory.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" namespace inet { namespace ieee80211 { -class INET_API TxopProcedure : public ModeSetListener +class INET_API TxopProcedure : public ModeSetModuleBase { public: static simsignal_t txopStartedSignal; diff --git a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.ned b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.ned index dc0c3403564..7c4cf8fb987 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.ned @@ -18,6 +18,7 @@ import inet.common.SimpleModule; simple TxopProcedure extends SimpleModule { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(TxopProcedure); double txopLimit @unit(s) = default(-1s); @display("i=block/timer"); diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.cc b/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.cc index a0ffcea12ed..866050cf176 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.cc +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.cc @@ -17,7 +17,7 @@ Define_Module(OriginatorProtectionMechanism); void OriginatorProtectionMechanism::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); } diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.h b/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.h index 1366ad71399..7a455d44d00 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.h +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.h @@ -9,13 +9,13 @@ #define __INET_ORIGINATORPROTECTIONMECHANISM_H #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" namespace inet { namespace ieee80211 { -class INET_API OriginatorProtectionMechanism : public ModeSetListener +class INET_API OriginatorProtectionMechanism : public ModeSetModuleBase { protected: IRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.ned b/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.ned index 6bd7cfc3340..b8311ff529b 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.ned +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.ned @@ -19,6 +19,7 @@ import inet.common.SimpleModule; simple OriginatorProtectionMechanism extends SimpleModule { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(OriginatorProtectionMechanism); string rateSelectionModule; @display("i=block/encrypt"); diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc index 4b38b531005..47009317dcb 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc @@ -18,7 +18,7 @@ Define_Module(SingleProtectionMechanism); void SingleProtectionMechanism::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); } diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h index 65ee94ccf3f..8f2d19d577f 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h @@ -8,7 +8,7 @@ #ifndef __INET_SINGLEPROTECTIONMECHANISM_H #define __INET_SINGLEPROTECTIONMECHANISM_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" @@ -24,7 +24,7 @@ namespace ieee80211 { // // 8.2.5.2 Setting for single and multiple protection under enhanced distributed channel access (EDCA) // -class INET_API SingleProtectionMechanism : public ModeSetListener +class INET_API SingleProtectionMechanism : public ModeSetModuleBase { protected: IQosRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.ned b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.ned index c9ff5857cf9..22f0a555eb3 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.ned +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.ned @@ -19,6 +19,7 @@ import inet.common.SimpleModule; simple SingleProtectionMechanism extends SimpleModule { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(SingleProtectionMechanism); string rateSelectionModule; @display("i=block/encrypt"); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned index 9ec36a21ebc..c63d6453ce9 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned @@ -18,6 +18,7 @@ import inet.linklayer.ieee80211.mac.contract.IRateControl; simple AarfRateControl extends SimpleModule like IRateControl { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(AarfRateControl); double initialRate @unit(bps) = default(-1bps); // -1 means the fastest mandatory rate double interval @unit(s) = default(50ms); // The rate (unconditionally) increases after each time interval. diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned index 966753c790f..bb02df4ff13 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned @@ -17,6 +17,7 @@ import inet.linklayer.ieee80211.mac.contract.IRateControl; simple OnoeRateControl extends SimpleModule like IRateControl { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(OnoeRateControl); double initialRate @unit(bps) = default(-1bps); // -1 means the fastest mandatory rate double interval @unit(s) = default(1s); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index 551a31c4c9e..f0d161ab4c3 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -19,7 +19,9 @@ simsignal_t RateControlBase::datarateChangedSignal = cComponent::registerSignal( void RateControlBase::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); + if (stage == INITSTAGE_LINK_LAYER) + resetRateControl(); } const IIeee80211Mode *RateControlBase::increaseRateIfPossible(const IIeee80211Mode *currentMode) @@ -60,15 +62,6 @@ void RateControlBase::emitDatarateChangedSignal(const MacAddress& receiver, cons } } -void RateControlBase::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) -{ - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - resetRateControl(); - } -} } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index 2a61b5028fa..5eb2fa2abf2 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -9,13 +9,13 @@ #define __INET_RATECONTROLBASE_H #include "inet/linklayer/common/MacAddress.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" namespace inet { namespace ieee80211 { -class INET_API RateControlBase : public ModeSetListener, public IRateControl +class INET_API RateControlBase : public ModeSetModuleBase, public IRateControl { public: static simsignal_t datarateChangedSignal; @@ -23,7 +23,6 @@ class INET_API RateControlBase : public ModeSetListener, public IRateControl protected: 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; // The receiver MAC address of a transmitted (or received) frame, which keys the per-station state. virtual MacAddress getReceiverAddress(Packet *frame) const; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 6408b443687..78eb9e886f2 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -49,12 +49,12 @@ static const IIeee80211Mode *getLegacyFallback(const Ieee80211ModeSet *modeSet, // a receiver-supported MCS/rate and CH_BANDWIDTH permitted by the BSS HT // Operation. The directional negotiated state is the model's source of the // receiver's capability advertisement. -static bool isCompatibleHtMode(const IIeee80211Mode *mode, const Ieee80211Mib::PeerHtState *peerHtState) +static bool isCompatibleHtMode(const IIeee80211Mode *mode, const Ieee80211Mib::PeerHtState *peerHtState, const Ieee80211HtOperation& operation) { if (mode == nullptr || peerHtState == nullptr || !peerHtState->valid) return false; - const auto& negotiated = peerHtState->negotiatedCapabilities; + const auto& negotiated = *peerHtState->negotiatedCapabilities; const auto& receiverCapabilities = negotiated.localTxPeerRx; if (!receiverCapabilities.valid) return false; @@ -65,7 +65,7 @@ static bool isCompatibleHtMode(const IIeee80211Mode *mode, const Ieee80211Mib::P auto bandwidth = mode->getDataMode()->getBandwidth(); if (receiverCapabilities.supportedChannelWidths.count(bandwidth) == 0 || - bandwidth > negotiated.operation.operatingChannelWidth) + bandwidth > operation.operatingChannelWidth) return false; // IEEE Std 802.11-2024, 10.17 and Table 9-224: a short guard interval @@ -105,7 +105,8 @@ static bool isBetterHtMode(const IIeee80211Mode *candidate, const IIeee80211Mode } // namespace const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, - const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode, const MacAddress& peerAddress) + const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode, const MacAddress& peerAddress, + const Ieee80211HtOperation *operation, bool htEligible) { if (mode == nullptr || mode->getHtMcsIndex() < 0) return mode; @@ -115,9 +116,10 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, throw cRuntimeError("HT mode '%s' is not contained in IEEE 802.11 mode set '%s'", mode->getName(), modeSet->getName()); - if (peerHtState == nullptr || !peerHtState->valid || !peerHtState->negotiatedCapabilities.localTxPeerRx.valid) + if (!htEligible || operation == nullptr || peerHtState == nullptr || !peerHtState->valid || + !peerHtState->negotiatedCapabilities || !peerHtState->negotiatedCapabilities->localTxPeerRx.valid) return getLegacyFallback(modeSet, mode, peerAddress); - if (isCompatibleHtMode(mode, peerHtState)) + if (isCompatibleHtMode(mode, peerHtState, *operation)) return mode; auto candidateBitrate = mode->getDataMode()->getNetBitrate(); @@ -126,7 +128,7 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, const auto *candidate = modeSet->getMode(i); if (candidate->getHtMcsIndex() < 0 || candidate->getDataMode()->getNetBitrate() > candidateBitrate || - !isCompatibleHtMode(candidate, peerHtState)) + !isCompatibleHtMode(candidate, peerHtState, *operation)) continue; if (bestMode == nullptr || isBetterHtMode(candidate, bestMode)) bestMode = candidate; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index ba5eaebf764..cd9bebfaf8c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -24,7 +24,8 @@ INET_API const physicallayer::IIeee80211Mode *selectPeerCompatibleMode( const physicallayer::Ieee80211ModeSet *modeSet, const Ieee80211Mib::PeerHtState *peerHtState, const physicallayer::IIeee80211Mode *mode, - const MacAddress& peerAddress); + const MacAddress& peerAddress, + const Ieee80211HtOperation *operation, bool htEligible); } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index add5ec700f1..953ceb5599c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -22,10 +22,11 @@ Define_Module(QosRateSelection); void QosRateSelection::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) mib.reference(this, "mibModule", true); if (stage == INITSTAGE_LINK_LAYER) { + fastestMandatoryMode = modeSet->getFastestMandatoryMode(); dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); double multicastFrameBitrate = par("multicastFrameBitrate"); multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); @@ -270,15 +271,6 @@ const IIeee80211Mode *QosRateSelection::computeMode(Packet *packet, const PtrgetReceiverAddress(), computeControlFrameMode(header, txopProcedure)); } -void QosRateSelection::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) -{ - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); - } -} void QosRateSelection::frameTransmitted(Packet *packet, const Ptr& header) { @@ -290,7 +282,8 @@ const IIeee80211Mode *QosRateSelection::getPeerCompatibleMode(const MacAddress& { if (mode == nullptr || peerAddress.isMulticast() || !mib || mode->getHtMcsIndex() < 0) return mode; - return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); + return selectPeerCompatibleMode(modeSet, mib->findPeerCapabilities(peerAddress), mode, peerAddress, + mib->hasHtOperation() ? &mib->getHtOperation() : nullptr, mib->relationshipAllowsHt(peerAddress)); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 569ef742d37..c84beacc05c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -9,7 +9,7 @@ #define __INET_QOSRATESELECTION_H #include "inet/common/ModuleRefByPar.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" @@ -30,13 +30,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 QosRateSelection : public IQosRateSelection, public ModeSetListener +class INET_API QosRateSelection : public IQosRateSelection, public ModeSetModuleBase { protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; - const physicallayer::Ieee80211ModeSet *modeSet = nullptr; std::map lastTransmittedFrameMode; // originator frame modes @@ -58,7 +57,6 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen protected: 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; // 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/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 35bf0e1ace5..5cb9f9bff87 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -21,6 +21,7 @@ import inet.common.SimpleModule; simple QosRateSelection extends SimpleModule { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(QosRateSelection); string rateControlModule; string mibModule = default("^.^.^.mib"); diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 9d28a5142cd..20b9522c46e 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -26,9 +26,9 @@ Define_Module(RateSelection); void RateSelection::initialize(int stage) { + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { mib.reference(this, "mibModule", true); - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); } else if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); @@ -184,15 +184,6 @@ const IIeee80211Mode *RateSelection::computeMode(Packet *packet, const Ptr(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); - } -} void RateSelection::frameTransmitted(Packet *packet, const Ptr& header) { @@ -223,7 +214,8 @@ const IIeee80211Mode *RateSelection::getPeerCompatibleMode(const MacAddress& pee { if (mode == nullptr || peerAddress.isMulticast() || !mib || mode->getHtMcsIndex() < 0) return mode; - return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); + return selectPeerCompatibleMode(modeSet, mib->findPeerCapabilities(peerAddress), mode, peerAddress, + mib->hasHtOperation() ? &mib->getHtOperation() : nullptr, mib->relationshipAllowsHt(peerAddress)); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 69441331453..3c407bcf694 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -9,7 +9,7 @@ #define __INET_RATESELECTION_H #include "inet/common/ModuleRefByPar.h" -#include "inet/common/SimpleModule.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" @@ -31,14 +31,13 @@ namespace ieee80211 { * 9.7.6.4 Rate selection for control frames that are not control response frames * 9.7.6.5 Rate selection for control response frames */ -class INET_API RateSelection : public IRateSelection, public SimpleModule, public cListener // FIXME +class INET_API RateSelection : public IRateSelection, public ModeSetModuleBase { protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; const physicallayer::IIeee80211Mode *fastestMandatoryMode = nullptr; - const physicallayer::Ieee80211ModeSet *modeSet = nullptr; std::map lastTransmittedFrameMode; // originator frame modes @@ -58,7 +57,6 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi protected: 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; // 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.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 89f63916421..2015d0669f8 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -17,6 +17,7 @@ import inet.linklayer.ieee80211.mac.contract.IRateSelection; simple RateSelection extends SimpleModule like IRateSelection { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(RateSelection); string rateControlModule; string mibModule = default("^.^.^.mib"); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.cc index 17edca5ffea..c8c1b5eb7e6 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.cc @@ -16,7 +16,7 @@ Define_Module(CtsPolicy); void CtsPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rx = check_and_cast(getModuleByPath(par("rxModule"))); rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.h index b86853715f2..3bd429aa4f5 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.h @@ -8,7 +8,7 @@ #ifndef __INET_CTSPOLICY_H #define __INET_CTSPOLICY_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/ICtsPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRx.h" @@ -16,7 +16,7 @@ namespace inet { namespace ieee80211 { -class INET_API CtsPolicy : public ModeSetListener, public ICtsPolicy +class INET_API CtsPolicy : public ModeSetModuleBase, public ICtsPolicy { protected: IRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.ned index d9d1f635d59..b7b04b55522 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/CtsPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.ICtsPolicy; simple CtsPolicy extends SimpleModule like ICtsPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(CtsPolicy); string rxModule; string rateSelectionModule; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.cc index afd8af57f9d..fd3ac6e86d6 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.cc @@ -16,7 +16,7 @@ Define_Module(QosCtsPolicy); void QosCtsPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rx = check_and_cast(getModuleByPath(par("rxModule"))); rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.h index b447b8ac786..bbf30022a2c 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.h @@ -15,7 +15,7 @@ namespace inet { namespace ieee80211 { -class INET_API QosCtsPolicy : public ModeSetListener, public ICtsPolicy +class INET_API QosCtsPolicy : public ModeSetModuleBase, public ICtsPolicy { protected: IRx *rx = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.ned index a2d61af6111..ce7d9085eeb 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/QosCtsPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.ICtsPolicy; simple QosCtsPolicy extends SimpleModule like ICtsPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(QosCtsPolicy); string rxModule; string rateSelectionModule; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.cc index af8077331bb..e1cd6e472da 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.cc @@ -16,7 +16,7 @@ Define_Module(RecipientAckPolicy); void RecipientAckPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.h index d851cf03ba7..ddae91281be 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.h @@ -8,14 +8,14 @@ #ifndef __INET_RECIPIENTACKPOLICY_H #define __INET_RECIPIENTACKPOLICY_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientAckPolicy.h" namespace inet { namespace ieee80211 { -class INET_API RecipientAckPolicy : public ModeSetListener, public IRecipientAckPolicy +class INET_API RecipientAckPolicy : public ModeSetModuleBase, public IRecipientAckPolicy { protected: IRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.ned index 4eb039a564f..837ce77b858 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientAckPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IRecipientAckPolicy; simple RecipientAckPolicy extends SimpleModule like IRecipientAckPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(RecipientAckPolicy); string rateSelectionModule; @display("i=block/control"); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 29f793f0110..4a42f5f2836 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -16,7 +16,7 @@ Define_Module(RecipientQosAckPolicy); void RecipientQosAckPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index 9f4ce456784..931635fc406 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -8,7 +8,7 @@ #ifndef __INET_RECIPIENTQOSACKPOLICY_H #define __INET_RECIPIENTQOSACKPOLICY_H -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientAckPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h" @@ -16,7 +16,7 @@ namespace inet { namespace ieee80211 { -class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipientAckPolicy, public IRecipientQosAckPolicy +class INET_API RecipientQosAckPolicy : public ModeSetModuleBase, public IRecipientAckPolicy, public IRecipientQosAckPolicy { protected: IQosRateSelection *rateSelection = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned index 50971923a80..ec3ceb199de 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IRecipientQosAckPolicy; simple RecipientQosAckPolicy extends SimpleModule like IRecipientQosAckPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(RecipientQosAckPolicy); string rateSelectionModule; @display("i=block/control"); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc index f98f3f8e699..7bb4edacdd0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc @@ -307,7 +307,7 @@ void Ieee80211AgentSta::processReassociateConfirm(Ieee80211Prim_ReassociateConfi { if (resp->getResultCode() != PRC_SUCCESS) { EV << "Reassociation error\n"; - bool isAssociated = mib->bssStationData.isAssociated; + bool isAssociated = mib->getBssStationData().isAssociated; emit(dropConfirmSignal, PR_REASSOCIATE_CONFIRM); if (!isAssociated) { EV << "Going back to scanning\n"; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.cc index 5b6b266b49e..f9671295849 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.cc @@ -21,6 +21,13 @@ void Ieee80211MgmtAdhoc::initialize(int stage) } } +void Ieee80211MgmtAdhoc::prepareLocalOperation() +{ + // This no-beacon abstraction has no learned BSS channel/HT operation or + // accepted peer advertisements. Keep that absence distinct from local HT support. + mib->commitBss("", MacAddress::UNSPECIFIED_ADDRESS, nullptr, -1, nullptr); +} + void Ieee80211MgmtAdhoc::handleTimer(cMessage *msg) { ASSERT(false); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.h index 45d285e9a85..45b1c899a77 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAdhoc.h @@ -24,6 +24,7 @@ class INET_API Ieee80211MgmtAdhoc : public Ieee80211MgmtBase protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; + void prepareLocalOperation() override; /** Implements abstract Ieee80211MgmtBase method */ virtual void handleTimer(cMessage *msg) override; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index 1b52306987e..c3e3106e75b 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -90,8 +90,6 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, cO auto transDetails = check_and_cast(details); frameTransmissionFinished(packet, transDetails->getStatus()); } - else - Ieee80211MgmtApBase::receiveSignal(source, signalID, obj, details); } Ieee80211MgmtAp::AssociationResponseDisposition Ieee80211MgmtAp::getAssociationResponseDisposition(const Packet *responseFrame, @@ -131,35 +129,34 @@ void Ieee80211MgmtAp::frameTransmissionFinished(const Packet *responseFrame, Fra if (status == FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED) { if (sta->second.pendingAssociationSuccessful) { - bool wasAssociated = mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED; + bool wasAssociated = mib->getPeerAssociationStatus(address) == Ieee80211Mib::ASSOCIATED; + // An acknowledged replacement starts a new relationship, even for equal capabilities. + mib->removePeerHtCapabilities(address); mib->commitAssociationId(address); - mib->bssAccessPointData.stations[address] = Ieee80211Mib::ASSOCIATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::ASSOCIATED); if (sta->second.pendingHtStateAvailable) { // IEEE Std 802.11-2024, 11.3.5.3: association state becomes effective only after the successful response exchange. - if (sta->second.pendingHtCapabilitiesValid && mib->isHtOperationSupported()) { - const auto& currentOperation = mib->getHtOperation(); - if (supportsBasicHtMcsSet(sta->second.pendingHtCapabilities, currentOperation)) - mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities, currentOperation); - else - mib->removePeerHtCapabilities(address); - } + if (sta->second.pendingHtCapabilitiesValid && mib->isLocalHtCapable()) + mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities); else mib->removePeerHtCapabilities(address); } clearPendingAssociation(&sta->second); + mib->publishStateChange(); // Signal delivery is synchronous; observers must see committed // station/peer state and no pending response transaction. if (!wasAssociated) sendAssocNotification(address); } - else if (mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED) { + else if (mib->getPeerAssociationStatus(address) == Ieee80211Mib::ASSOCIATED) { // This model does not implement negotiated management-frame protection. // IEEE Std 802.11-2024, 11.3.5.3(p) for association and 11.3.5.5(n) // for same-AP reassociation therefore require the existing association // state to be cleared after this acknowledged refusal. mib->releaseAssociationId(address); - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); clearPendingAssociation(&sta->second); + mib->publishStateChange(); // Signal delivery is synchronous; observers must see the complete // downgraded state and no pending response transaction. sendDisAssocNotification(address); @@ -216,7 +213,7 @@ void Ieee80211MgmtAp::sendBeacon() body->setBeaconInterval(beaconInterval); body->setChannelNumber(getDsssParameterSetChannel()); addHtCapabilities(body); - if (mib->isHtOperationSupported()) + if (mib->isLocalHtCapable()) setHtOperation(body, getHtOperationBand(), mib->getHtOperation()); body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (body->getChannelNumber() != -1 ? 3 : 0)) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); sendManagementFrame("Beacon", body, ST_BEACON, MacAddress::BROADCAST_ADDRESS); @@ -234,7 +231,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrgetTransmitterAddress(); sta = &staList[staAddress]; // this implicitly creates a new entry sta->address = staAddress; - mib->bssAccessPointData.stations[staAddress] = Ieee80211Mib::NOT_AUTHENTICATED; + mib->setPeerAssociationStatus(staAddress, Ieee80211Mib::NOT_AUTHENTICATED); sta->authSeqExpected = 1; } // reset authentication status, when starting a new auth sequence @@ -250,10 +247,10 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + bool wasAssociated = mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); - mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; + mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::NOT_AUTHENTICATED); mib->removePeerHtCapabilities(sta->address); sta->authSeqExpected = 1; if (wasAssociated) @@ -289,10 +286,10 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + bool wasAssociated = mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); - mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::AUTHENTICATED; // TODO only when ACK of this frame arrives + mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::AUTHENTICATED); // TODO only when ACK of this frame arrives mib->removePeerHtCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); @@ -313,11 +310,11 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + bool wasAssociated = mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; // mark STA as not authenticated; alternatively, it could also be removed from staList if (wasAssociated) mib->releaseAssociationId(sta->address); - mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; + mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::NOT_AUTHENTICATED); sta->authSeqExpected = 1; mib->removePeerHtCapabilities(sta->address); if (wasAssociated) @@ -340,7 +337,7 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::NOT_AUTHENTICATED) { + if (!sta || mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::NOT_AUTHENTICATED) { // STA not authenticated: send error and return const auto& body = makeShared(); body->setReasonCode(RC_NONAUTH_ASS_REQUEST); @@ -350,7 +347,7 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrpeekData(); - bool pendingHtOperationValid = mib->isHtOperationSupported(); + bool pendingHtOperationValid = mib->isLocalHtCapable(); Ieee80211HtOperation pendingHtOperation; if (pendingHtOperationValid) pendingHtOperation = mib->getHtOperation(); @@ -420,7 +417,7 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< } if (sta != nullptr) clearPendingAssociation(sta); - if (!sta || mib->bssAccessPointData.stations[sta->address] == Ieee80211Mib::NOT_AUTHENTICATED) { + if (!sta || mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::NOT_AUTHENTICATED) { // STA not authenticated: send error and return const auto& body = makeShared(); body->setReasonCode(RC_NONAUTH_ASS_REQUEST); @@ -430,7 +427,7 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< } const auto& requestBody = packet->peekData(); - bool pendingHtOperationValid = mib->isHtOperationSupported(); + bool pendingHtOperationValid = mib->isLocalHtCapable(); Ieee80211HtOperation pendingHtOperation; if (pendingHtOperationValid) pendingHtOperation = mib->getHtOperation(); @@ -494,10 +491,10 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + bool wasAssociated = mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); - mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::AUTHENTICATED); mib->removePeerHtCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); @@ -530,7 +527,7 @@ void Ieee80211MgmtAp::handleProbeRequestFrame(Packet *packet, const PtrsetBeaconInterval(beaconInterval); body->setChannelNumber(getDsssParameterSetChannel()); addHtCapabilities(body); - if (mib->isHtOperationSupported()) + if (mib->isLocalHtCapable()) setHtOperation(body, getHtOperationBand(), mib->getHtOperation()); body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (body->getChannelNumber() != -1 ? 3 : 0)) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); sendManagementFrame("ProbeResp", body, ST_PROBERESPONSE, staAddress); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned index 95397135f3b..97553d8ae4a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned @@ -8,6 +8,7 @@ package inet.linklayer.ieee80211.mgmt; import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.contract.IIeee80211BssProvider; // // Used in 802.11 infrastructure mode in an access point (AP). @@ -20,7 +21,7 @@ import inet.common.SimpleModule; // This module relies on a connected ~Ieee80211Mac for actual // reception and transmission of frames. // -simple Ieee80211MgmtAp extends SimpleModule like IIeee80211Mgmt +simple Ieee80211MgmtAp extends SimpleModule like IIeee80211Mgmt, IIeee80211BssProvider { parameters: @class(Ieee80211MgmtAp); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc index 8472d749736..8a1f7ef1f1a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc @@ -34,20 +34,56 @@ void Ieee80211MgmtApBase::initialize(int stage) Ieee80211MgmtBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { mib->mode = Ieee80211Mib::INFRASTRUCTURE; - mib->bssStationData.stationType = Ieee80211Mib::ACCESS_POINT; - mib->bssData.ssid = par("ssid").stdstringValue(); + mib->configureBssRole(Ieee80211Mib::ACCESS_POINT, par("ssid").stdstringValue()); radio = getModuleFromPar(par("radioModule"), this); radio->subscribe(ieee80211RadioChannelChangedSignal, this); } - else if (stage == INITSTAGE_LINK_LAYER) - mib->bssData.bssid = mib->address; - else if (stage == INITSTAGE_LAST && mib->isHtOperationSupported()) { - mib->setPrimaryChannel(mib->requirePrimaryChannel(), getHtOperationBand()); - const auto& operation = mib->getHtOperation(); - if (operation.operatingChannelWidth == MHz(40) && - !getHtOperationBand()->isHt40OperationSupported(operation.primaryChannel, operation.secondaryChannelOffset)) - throw cRuntimeError("Invalid 40 MHz HT operation for band '%s', primary channel index %d, secondary channel offset %d", - getHtOperationBand()->getName(), operation.primaryChannel, operation.secondaryChannelOffset); + +} + +void Ieee80211MgmtApBase::prepareBss() +{ + Enter_Method("prepareBss"); + if (!isUp()) + throw cRuntimeError("Cannot prepare a BSS while AP management is down"); + prepareConfiguration(); + prepareLocalOperation(); +} + +void Ieee80211MgmtApBase::installSimplifiedPeer(const MacAddress& address, const Ieee80211HtCapabilities *capabilities) +{ + Enter_Method("installSimplifiedPeer"); + prepareBss(); + // This method is the explicit no-air association completion boundary. + mib->setPeerAssociationStatus(address, Ieee80211Mib::ASSOCIATED); + if (capabilities != nullptr && mib->isLocalHtCapable()) + mib->setPeerHtCapabilities(address, *capabilities); + else + mib->removePeerHtCapabilities(address); + mib->publishStateChange(); +} + +void Ieee80211MgmtApBase::removeSimplifiedPeer(const MacAddress& address) +{ + Enter_Method("removeSimplifiedPeer"); + mib->removePeerAssociation(address); + mib->publishStateChange(); +} + +void Ieee80211MgmtApBase::prepareLocalOperation() +{ + if (mib->isLocalHtCapable()) { + int channel = radioChannel; + if (channel < 0) + throw cRuntimeError("IEEE 802.11 primary channel is unavailable"); + const auto *band = getHtOperationBand(); + band->getStandardChannelNumber(channel); + auto operation = computeLocalHtOperation(channel, band); + mib->commitBss(mib->getBssData().ssid, mib->address, band, channel, &operation); + } + else { + mib->commitBss(mib->getBssData().ssid, mib->address, radioBand, + radioChannel, nullptr); } } @@ -59,14 +95,20 @@ void Ieee80211MgmtApBase::receiveSignal(cComponent *source, simsignal_t signalID EV << "Updating AP primary channel to " << value << ".\n"; const auto *channelDetails = dynamic_cast(details); const auto *band = channelDetails == nullptr ? nullptr : channelDetails->getBand(); - if (mib->isHtOperationSupported()) { + if (value < 0 || value > 255) + throw cRuntimeError("IEEE 802.11 primary channel must be in the range 0..255"); + if (mib->isLocalHtCapable()) { if (band == nullptr) throw cRuntimeError("HT Operation channel conversion requires radioChannelChanged with IEEE 802.11 band details"); - mib->setPrimaryChannel(value, band); + band->getStandardChannelNumber(value); } - else - mib->setPrimaryChannel(value); + // Physical context is retained while down; it does not activate a BSS. radioBand = band; + radioChannel = value; + if (mib->hasPreparedLocalCapabilities() && isUp()) { + prepareLocalOperation(); + mib->publishStateChange(); + } } } @@ -92,7 +134,7 @@ int Ieee80211MgmtApBase::getDsssParameterSetChannel() const return radioBand->getStandardChannelNumber(channelIndex); } catch (const cRuntimeError&) { - if (mib->isHtOperationSupported()) + if (mib->isLocalHtCapable()) throw; // Modeling simplification: nonstandard legacy bands can operate without // a standards channel mapping. Omit DSSS rather than invent a wire value. diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h index 6fb1cdfc3e8..0ab191b7e35 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h @@ -10,6 +10,7 @@ #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h" +#include "inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.h" namespace inet { @@ -27,10 +28,16 @@ namespace ieee80211 { * with utility functions that are useful for implementing AP functionality. * */ -class INET_API Ieee80211MgmtApBase : public Ieee80211MgmtBase +class INET_API Ieee80211MgmtApBase : public Ieee80211MgmtBase, public IIeee80211BssProvider { + public: + void prepareBss() override; + void installSimplifiedPeer(const MacAddress& address, const Ieee80211HtCapabilities *capabilities) override; + void removeSimplifiedPeer(const MacAddress& address) override; + protected: cModule *radio = nullptr; + int radioChannel = -1; const physicallayer::IIeee80211Band *radioBand = nullptr; // Immutable band observed via radioChannelChanged const physicallayer::IIeee80211Band *getHtOperationBand() const; @@ -38,6 +45,7 @@ class INET_API Ieee80211MgmtApBase : public Ieee80211MgmtBase virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; + void prepareLocalOperation() override; using Ieee80211MgmtBase::receiveSignal; virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; }; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApSimplified.ned b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApSimplified.ned index c1926380be1..cf8b1f96940 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApSimplified.ned +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApSimplified.ned @@ -8,6 +8,7 @@ package inet.linklayer.ieee80211.mgmt; import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.contract.IIeee80211BssProvider; // // Used in 802.11 infrastructure mode in an access point (AP). @@ -15,7 +16,7 @@ import inet.common.SimpleModule; // This management module variant does not send or expect to receive any // management frames -- it simply treats all stations as associated all the time. // -simple Ieee80211MgmtApSimplified extends SimpleModule like IIeee80211Mgmt +simple Ieee80211MgmtApSimplified extends SimpleModule like IIeee80211Mgmt, IIeee80211BssProvider { parameters: @class(Ieee80211MgmtApSimplified); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 3c3bc33903a..4ff5f53563e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -34,55 +34,94 @@ void Ieee80211MgmtBase::initialize(int stage) myIface = getContainingNicModule(this); numMgmtFramesReceived = 0; numMgmtFramesDropped = 0; - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); + configurationProvider.reference(this, "macModule", true); WATCH(numMgmtFramesReceived); WATCH(numMgmtFramesDropped); } + else if (stage == INITSTAGE_LINK_LAYER) { + prepareConfiguration(); + if (isUp()) + prepareLocalOperation(); + mib->publishStateChange(); + } } -void Ieee80211MgmtBase::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void Ieee80211MgmtBase::prepareConfiguration() { - Enter_Method("%s", cComponent::getSignalName(signalID)); - - 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()); + if (configurationPrepared) + return; + configurationProvider->prepareLocalCapabilities(); + modeSet = configurationProvider->getConfiguredModeSet(); + if (modeSet == nullptr) + throw cRuntimeError("Configured IEEE 802.11 mode catalog is unavailable"); + configurationPrepared = true; + 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; +} + +Ieee80211HtOperation Ieee80211MgmtBase::computeLocalHtOperation(int primaryChannel, const IIeee80211Band *band) const +{ + Ieee80211HtOperation operation; + operation.primaryChannel = primaryChannel; + int offset = mib->par("htSecondaryChannelOffset"); + if (offset != 0 && offset != 1 && offset != 3) + throw cRuntimeError("htSecondaryChannelOffset must be 0, 1, or 3"); + if (offset != 0 && mib->getLocalHtCapabilities().supportedChannelWidths.count(MHz(40)) == 0) + throw cRuntimeError("40 MHz HT operation requires a configured PHY that can operate a 40 MHz channel width"); + if (offset != 0 && band != nullptr && !band->isHt40OperationSupported(primaryChannel, offset)) { + EV_WARN << "Configured 40 MHz HT operation is unsupported on primary channel " << primaryChannel + << " in band '" << band->getName() << "'; falling back to 20 MHz BSS operation.\n"; + offset = 0; } + operation.secondaryChannelOffset = offset; + operation.operatingChannelWidth = offset != 0 ? MHz(40) : MHz(20); + int protection = mib->par("htProtectionMode"); + if (protection < 0 || protection > 3) + throw cRuntimeError("htProtectionMode must be between 0 and 3"); + operation.protectionMode = static_cast(protection); + const auto& mandatory = modeSet->getHtMcsMandatory(); + for (int mcs = 0; mcs < 77; mcs++) + operation.basicMcsSupported[mcs] = mandatory[mcs] && mib->getLocalHtCapabilities().rxMcsSupported[mcs]; + return operation; +} + +void Ieee80211MgmtBase::prepareLocalOperation() +{ + } void Ieee80211MgmtBase::addHtCapabilities(const Ptr& frame) const { - if (mib->isHtOperationSupported()) - setHtCapabilities(frame, mib->localHtCapabilities); + if (mib->isLocalHtCapable()) + setHtCapabilities(frame, mib->getLocalHtCapabilities()); } void Ieee80211MgmtBase::addHtOperation(const Ptr& frame, const physicallayer::IIeee80211Band *band) const { - if (mib->isHtOperationSupported()) + if (mib->isLocalHtCapable()) setHtOperation(frame, band, mib->getHtOperation()); } @@ -111,6 +150,7 @@ void Ieee80211MgmtBase::handleMessageWhenUp(cMessage *msg) } else throw cRuntimeError("Unknown message"); + mib->publishStateChange(); } void Ieee80211MgmtBase::sendDown(Packet *frame) @@ -187,11 +227,14 @@ void Ieee80211MgmtBase::processFrame(Packet *packet, const PtrclearPeerHtCapabilities(); + mib->clearBss(); + mib->publishStateChange(); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index 145da89e902..9527212cd75 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -14,6 +14,7 @@ #include "inet/common/packet/Packet.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/networklayer/contract/IInterfaceTable.h" @@ -35,7 +36,9 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener ModuleRefByPar mib; ModuleRefByPar interfaceTable; NetworkInterface *myIface = nullptr; - physicallayer::Ieee80211ModeSet *modeSet = nullptr; + ModuleRefByPar configurationProvider; + const physicallayer::Ieee80211ModeSet *modeSet = nullptr; + bool configurationPrepared = false; Ieee80211SupportedRatesElement supportedRates; Ieee80211ExtendedSupportedRatesElement extendedSupportedRates; @@ -46,7 +49,7 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + void prepareConfiguration(); /** Dispatches incoming messages to handleTimer(), handleUpperMessage() or processFrame(). */ virtual void handleMessageWhenUp(cMessage *msg) override; @@ -83,6 +86,9 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener } /** Adds the local HT advertisement to a frame when the authoritative PHY profile supports HT operation. */ + Ieee80211HtOperation computeLocalHtOperation(int primaryChannel, const physicallayer::IIeee80211Band *band) const; + virtual void prepareLocalOperation(); + virtual void addHtCapabilities(const Ptr& frame) const; virtual void addHtOperation(const Ptr& frame, const physicallayer::IIeee80211Band *band) const; @@ -109,7 +115,7 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener virtual bool isModuleStartStage(int stage) const override { return stage == ModuleStartOperation::STAGE_PHYSICAL_LAYER; } virtual bool isModuleStopStage(int stage) const override { return stage == ModuleStopOperation::STAGE_PHYSICAL_LAYER; } - virtual void handleStartOperation(LifecycleOperation *operation) override { start(); } + void handleStartOperation(LifecycleOperation *operation) override { start(); mib->publishStateChange(); } virtual void handleStopOperation(LifecycleOperation *operation) override { stop(); } virtual void handleCrashOperation(LifecycleOperation *operation) override { stop(); } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index 859a5c108ea..afa0c3221f5 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -152,8 +152,8 @@ void Ieee80211MgmtSta::initialize(int stage) if (stage == INITSTAGE_LOCAL) { mib->mode = Ieee80211Mib::INFRASTRUCTURE; - mib->bssStationData.stationType = Ieee80211Mib::STATION; - mib->bssStationData.isAssociated = false; + mib->configureBssRole(Ieee80211Mib::STATION); + mib->setAssociated(false); isScanning = false; assocTimeoutMsg = nullptr; @@ -352,7 +352,7 @@ void Ieee80211MgmtSta::startAuthentication(ApInfo *ap, simtime_t timeout) void Ieee80211MgmtSta::startAssociation(ApInfo *ap, simtime_t timeout) { - if (mib->bssStationData.isAssociated || assocTimeoutMsg) + if (mib->getBssStationData().isAssociated || assocTimeoutMsg) throw cRuntimeError("startAssociation: already associated or association currently in progress"); if (!ap->isAuthenticated) throw cRuntimeError("startAssociation: not yet authenticated with AP address='%s'", ap->address.str().c_str()); @@ -378,7 +378,7 @@ void Ieee80211MgmtSta::startAssociation(ApInfo *ap, simtime_t timeout) void Ieee80211MgmtSta::startReassociation(ApInfo *ap, simtime_t timeout) { - if (!mib->bssStationData.isAssociated || assocTimeoutMsg) + if (!mib->getBssStationData().isAssociated || assocTimeoutMsg) throw cRuntimeError("startReassociation: not associated or association currently in progress"); if (!ap->isAuthenticated) throw cRuntimeError("startReassociation: not authenticated with AP address='%s'", ap->address.str().c_str()); @@ -416,7 +416,7 @@ void Ieee80211MgmtSta::processScanCommand(Ieee80211Prim_ScanRequest *ctrl) if (isScanning) throw cRuntimeError("processScanCommand: scanning already in progress"); - if (mib->bssStationData.isAssociated) + if (mib->getBssStationData().isAssociated) disassociate(); // clear existing AP list (and cancel any pending authentications) -- we want to start with a clean page @@ -532,7 +532,7 @@ void Ieee80211MgmtSta::processDeauthenticateCommand(Ieee80211Prim_Deauthenticate if (!ap) throw cRuntimeError("processDeauthenticateCommand: AP not known: address = %s", address.str().c_str()); - if (mib->bssStationData.isAssociated && assocAP.address == address) + if (mib->getBssStationData().isAssociated && assocAP.address == address) disassociate(); else if (assocTimeoutMsg && assocTimeoutMsg->getContextPointer() == ap) cancelPendingAssociation(); @@ -570,7 +570,7 @@ void Ieee80211MgmtSta::processAssociateCommand(Ieee80211Prim_AssociateRequest *c void Ieee80211MgmtSta::processReassociateCommand(Ieee80211Prim_ReassociateRequest *ctrl) { const MacAddress& address = ctrl->getAddress(); - if (!mib->bssStationData.isAssociated) { + if (!mib->getBssStationData().isAssociated) { auto confirm = new Ieee80211Prim_ReassociateConfirm(); confirm->setAddress(address); sendConfirm(confirm, PRC_REFUSED); @@ -592,7 +592,7 @@ void Ieee80211MgmtSta::processDisassociateCommand(Ieee80211Prim_DisassociateRequ { const MacAddress& address = ctrl->getAddress(); - if (mib->bssStationData.isAssociated && address == assocAP.address) { + if (mib->getBssStationData().isAssociated && address == assocAP.address) { disassociate(); } else if (assocTimeoutMsg) { @@ -613,24 +613,26 @@ void Ieee80211MgmtSta::processDisassociateCommand(Ieee80211Prim_DisassociateRequ void Ieee80211MgmtSta::disassociate() { EV << "Disassociating from AP address=" << assocAP.address << "\n"; - ASSERT(mib->bssStationData.isAssociated); + ASSERT(mib->getBssStationData().isAssociated); cancelPendingAssociation(); clearCurrentAssociation(); } void Ieee80211MgmtSta::clearCurrentAssociation() { - ASSERT(mib->bssStationData.isAssociated); - mib->bssStationData.isAssociated = false; + ASSERT(mib->getBssStationData().isAssociated); + mib->setAssociated(false); mib->removePeerHtCapabilities(assocAP.address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; assocAP = AssociatedApInfo(); // clear it + mib->clearBss(); + mib->publishStateChange(); } bool Ieee80211MgmtSta::terminateCurrentAssociationFromPeer(const MacAddress& address) { - if (!mib->bssStationData.isAssociated || address != assocAP.address) + if (!mib->getBssStationData().isAssociated || address != assocAP.address) return false; // Keep a stable AP-list object for the primitive confirmation while the @@ -665,7 +667,7 @@ void Ieee80211MgmtSta::stop() clearAPList(); - if (mib->bssStationData.isAssociated) + if (mib->getBssStationData().isAssociated) clearCurrentAssociation(); else { cancelAndDelete(assocAP.beaconTimeoutMsg); @@ -793,7 +795,7 @@ void Ieee80211MgmtSta::handleDeauthenticationFrame(Packet *packet, const Ptr(assocTimeoutMsg->getContextPointer()) : nullptr; bool isPendingAp = pendingAp != nullptr && pendingAp->address == address; - bool isCurrentAp = mib->bssStationData.isAssociated && address == assocAP.address; + bool isCurrentAp = mib->getBssStationData().isAssociated && address == assocAP.address; // IEEE Std 802.11-2024, 11.3.4.5: deauthentication from the current AP // returns the STA to State 1. Do this before consulting the discovery @@ -922,15 +924,15 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrbssStationData.isAssociated || assocAP.address != ap->address) + else if (!mib->getBssStationData().isAssociated || assocAP.address != ap->address) mib->removePeerHtCapabilities(ap->address); } else { EV << "Association successful, AP address=" << ap->address << "\n"; - if (mib->bssStationData.isAssociated) { + if (mib->getBssStationData().isAssociated) { EV << "Breaking existing association with AP address=" << assocAP.address << "\n"; - mib->bssStationData.isAssociated = false; + mib->setAssociated(false); mib->removePeerHtCapabilities(assocAP.address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; @@ -938,12 +940,15 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrbssData.ssid = ap->ssid; - mib->bssData.bssid = ap->address; - mib->bssStationData.isAssociated = true; + mib->commitBss(ap->ssid, ap->address, band != nullptr ? band : ap->band, + responseHtStatus == HtAssociationResponseStatus::VALID_HT ? responseHtOperation.primaryChannel : ap->channel, + responseHtStatus == HtAssociationResponseStatus::VALID_HT ? &responseHtOperation : nullptr); + mib->setAssociated(true); (ApInfo&)assocAP = (*ap); + assocAP.beaconTimeoutMsg = new cMessage("beaconTimeout", MK_BEACON_TIMEOUT); + scheduleAfter(MAX_BEACONS_MISSED * assocAP.beaconInterval, assocAP.beaconTimeoutMsg); if (responseHtStatus == HtAssociationResponseStatus::VALID_HT) - mib->setPeerHtCapabilities(ap->address, responseHtCapabilities, responseHtOperation); + mib->setPeerHtCapabilities(ap->address, responseHtCapabilities); else { mib->removePeerHtCapabilities(ap->address); if (responseHtStatus == HtAssociationResponseStatus::INVALID_HT) { @@ -958,10 +963,8 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrpublishStateChange(); emit(l2AssociatedSignal, myIface, ap); - - assocAP.beaconTimeoutMsg = new cMessage("beaconTimeout", MK_BEACON_TIMEOUT); - scheduleAfter(MAX_BEACONS_MISSED * assocAP.beaconInterval, assocAP.beaconTimeoutMsg); } // report back to agent @@ -983,7 +986,7 @@ Ieee80211MgmtSta::HtAssociationResponseStatus Ieee80211MgmtSta::classifyAssociat // A non-HT station deliberately ignores HT elements. This preserves the // genuine legacy association path even when an HT-capable AP includes its // normal response elements. - if (!mib->isHtOperationSupported()) + if (!mib->isLocalHtCapable()) return HtAssociationResponseStatus::LEGACY; if (!responseHtCapabilitiesPresent && !responseHtOperationPresent) return HtAssociationResponseStatus::LEGACY; @@ -1016,8 +1019,8 @@ Ieee80211MgmtSta::HtAssociationResponseStatus Ieee80211MgmtSta::classifyAssociat // Reassociation Responses. Use the selected BSS's Beacon/Probe Response // advertisement instead of interpreting those reserved response bits. responseHtOperation.basicMcsSupported = selectedBssHtOperation->basicMcsSupported; - auto negotiated = negotiateHtCapabilities(mib->localHtCapabilities, responseHtCapabilities, responseHtOperation); - if (!supportsBasicHtMcsSet(mib->localHtCapabilities, responseHtOperation) || + auto negotiated = negotiateHtCapabilities(mib->getLocalHtCapabilities(), responseHtCapabilities); + if (!supportsBasicHtMcsSet(mib->getLocalHtCapabilities(), responseHtOperation) || !negotiated.localTxPeerRx.valid || !negotiated.localRxPeerTx.valid) { reason = "HT capabilities and operation have no bidirectionally usable common mode"; return HtAssociationResponseStatus::INVALID_HT; @@ -1027,7 +1030,7 @@ Ieee80211MgmtSta::HtAssociationResponseStatus Ieee80211MgmtSta::classifyAssociat bool Ieee80211MgmtSta::isHtBssSupported(const ApInfo *ap, std::string& reason) const { - if (!mib->isHtOperationSupported()) + if (!mib->isLocalHtCapable()) return true; if (!ap->htCapabilitiesPresent && !ap->htOperationPresent) return true; @@ -1035,8 +1038,8 @@ bool Ieee80211MgmtSta::isHtBssSupported(const ApInfo *ap, std::string& reason) c reason = "selected BSS contains only one of the HT Capabilities and HT Operation elements"; return false; } - auto negotiated = negotiateHtCapabilities(mib->localHtCapabilities, ap->htCapabilities, ap->htOperation); - if (!supportsBasicHtMcsSet(mib->localHtCapabilities, ap->htOperation) || + auto negotiated = negotiateHtCapabilities(mib->getLocalHtCapabilities(), ap->htCapabilities); + if (!supportsBasicHtMcsSet(mib->getLocalHtCapabilities(), ap->htOperation) || !negotiated.localTxPeerRx.valid || !negotiated.localRxPeerTx.valid) { reason = "selected BSS HT advertisement has no bidirectionally usable common mode"; return false; @@ -1058,11 +1061,11 @@ void Ieee80211MgmtSta::handleReassociationFailure(ApInfo *ap) { // IEEE Std 802.11-2024, 11.3.5.4(f): failed or timed-out reassociation // disassociates the STA only when the target is its current AP. - if (shouldDisassociateOnReassociationFailure(mib->bssStationData.isAssociated, assocAP.address, ap->address)) + if (shouldDisassociateOnReassociationFailure(mib->getBssStationData().isAssociated, assocAP.address, ap->address)) disassociate(); else { mib->removePeerHtCapabilities(ap->address); - if (mib->bssStationData.isAssociated) + if (mib->getBssStationData().isAssociated) changeChannel(assocAP.channel); } } @@ -1107,7 +1110,7 @@ void Ieee80211MgmtSta::handleBeaconFrame(Packet *packet, const PtrbssStationData.isAssociated && header->getTransmitterAddress() == assocAP.address) { + if (accepted && mib->getBssStationData().isAssociated && header->getTransmitterAddress() == assocAP.address) { EV << "Beacon is from associated AP, restarting beacon timeout timer\n"; ASSERT(assocAP.beaconTimeoutMsg != nullptr); rescheduleAfter(MAX_BEACONS_MISSED * assocAP.beaconInterval, assocAP.beaconTimeoutMsg); @@ -1116,6 +1119,7 @@ void Ieee80211MgmtSta::handleBeaconFrame(Packet *packet, const PtrpublishStateChange(); delete packet; } @@ -1154,10 +1158,11 @@ bool Ieee80211MgmtSta::storeAPInfo(Packet *packet, const PtrgetExtendedSupportedRates(); bool htCapabilitiesPresent = body->getHtCapabilitiesPresent(); bool htOperationPresent = body->getHtOperationPresent(); - bool ignoreHt = mib != nullptr && !mib->isHtOperationSupported(); + bool ignoreHt = mib != nullptr && !mib->isLocalHtCapable(); std::string reason; const auto& channelInd = packet->findTag(); const auto *receivedChannel = channelInd != nullptr ? channelInd->getChannel() : nullptr; + candidate.band = receivedChannel != nullptr ? receivedChannel->getBand() : nullptr; // IEEE Std 802.11-2024, 9.4.2.4: Current Channel is a standard // channel number. Without this optional element, use the receive channel. int legacyChannel = receivedChannel != nullptr ? receivedChannel->getChannelNumber() : -1; @@ -1235,6 +1240,7 @@ bool Ieee80211MgmtSta::storeAPInfo(Packet *packet, const Ptrchannel = candidate.channel; + ap->band = candidate.band; ap->address = candidate.address; ap->ssid = candidate.ssid; ap->supportedRates = candidate.supportedRates; @@ -1249,23 +1255,17 @@ bool Ieee80211MgmtSta::storeAPInfo(Packet *packet, const PtrisAuthenticated = candidate.isAuthenticated; ap->authSeqExpected = candidate.authSeqExpected; ap->authTimeoutMsg = candidate.authTimeoutMsg; - bool currentAssociatedAp = mib != nullptr && mib->bssStationData.isAssociated && address == assocAP.address; + bool currentAssociatedAp = mib != nullptr && mib->getBssStationData().isAssociated && address == assocAP.address; bool isBeacon = header->getType() == ST_BEACON || (header->getType() != ST_PROBERESPONSE && dynamicPtrCast(body) == nullptr); if (currentAssociatedAp && isBeacon) { (ApInfo&)assocAP = *ap; - mib->bssData.ssid = ap->ssid; - if (mib->isHtOperationSupported() && candidate.htCapabilitiesPresent && candidate.htOperationPresent) { - auto negotiated = negotiateHtCapabilities(mib->localHtCapabilities, candidate.htCapabilities, candidate.htOperation); - if (supportsBasicHtMcsSet(mib->localHtCapabilities, candidate.htOperation) && - negotiated.localTxPeerRx.valid && negotiated.localRxPeerTx.valid) { - EV_INFO << "Refreshing authoritative HT state for associated AP address=" << address << "\n"; - mib->setPeerHtCapabilities(address, candidate.htCapabilities, candidate.htOperation); - } - else { - EV_WARN << "Beacon from associated AP has unusable HT advertisement: removing peer HT state for AP address=" << address << "\n"; - mib->removePeerHtCapabilities(address); - } + mib->commitBss(ap->ssid, ap->address, ap->band, ap->channel, + candidate.htOperationPresent ? &candidate.htOperation : nullptr); + if (mib->isLocalHtCapable() && candidate.htCapabilitiesPresent && candidate.htOperationPresent) { + mib->setPeerHtCapabilities(address, candidate.htCapabilities); + if (!mib->relationshipAllowsHt(address)) + EV_WARN << "Beacon from associated AP has unusable HT operation; retaining capability knowledge.\n"; } else { EV_INFO << "Beacon from associated AP has no usable HT advertisement or STA is legacy: removing peer HT state for AP address=" << address << "\n"; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h index 326c7638644..a2c4c6ce47a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h @@ -72,6 +72,7 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase // struct ApInfo : public cObject { int channel; // internal zero-based radio channel index + const physicallayer::IIeee80211Band *band = nullptr; MacAddress address; // alias bssid std::string ssid; Ieee80211SupportedRatesElement supportedRates; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc index 614ea26bb49..3a7b775e0f1 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h" #include "inet/networklayer/common/L3AddressResolver.h" +#include "inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.h" namespace inet { @@ -47,14 +48,12 @@ void Ieee80211MgmtStaSimplified::initialize(int stage) Ieee80211MgmtBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { mib->mode = Ieee80211Mib::INFRASTRUCTURE; - mib->bssStationData.stationType = Ieee80211Mib::STATION; - mib->bssStationData.isAssociated = true; + mib->configureBssRole(Ieee80211Mib::STATION); } else if (stage == INITSTAGE_LINK_LAYER) { - configureAssociation(); + if (isUp()) + configureAssociation(); } - else if (stage == INITSTAGE_LAST) - configureAssociation(); } void Ieee80211MgmtStaSimplified::handleStartOperation(LifecycleOperation *operation) @@ -68,26 +67,31 @@ void Ieee80211MgmtStaSimplified::configureAssociation() { L3AddressResolver addressResolver; auto accessPointAddress = addressResolver.resolve(par("accessPointAddress"), L3AddressResolver::ADDR_MAC).toMac(); - mib->bssData.bssid = accessPointAddress; auto apMib = findAccessPointMib(accessPointAddress); - apMib->bssAccessPointData.stations[mib->address] = Ieee80211Mib::ASSOCIATED; - mib->bssData.ssid = apMib->bssData.ssid; - mib->bssStationData.isAssociated = true; + auto *apManagement = check_and_cast(apMib->getParentModule()->getSubmodule("mgmt")); + apManagement->prepareBss(); + mib->commitBss(apMib->getBssData().ssid, accessPointAddress, apMib->getOperationBand(), + apMib->hasPrimaryChannel() ? apMib->requirePrimaryChannel() : -1, + mib->isLocalHtCapable() && apMib->hasHtOperation() ? &apMib->getHtOperation() : nullptr); + mib->setAssociated(true); // Simplified management is an explicit no-air abstraction: install the state that the // Association Request/Response exchange would have committed in detailed management. - if (mib->isHtOperationSupported() && apMib->isHtOperationSupported()) { - mib->setPeerHtCapabilities(apMib->address, apMib->localHtCapabilities, apMib->getHtOperation()); - apMib->setPeerHtCapabilities(mib->address, mib->localHtCapabilities, apMib->getHtOperation()); + if (mib->isLocalHtCapable() && apMib->isLocalHtCapable()) { + mib->setPeerHtCapabilities(apMib->address, apMib->getLocalHtCapabilities()); + } + apManagement->installSimplifiedPeer(mib->address, mib->isLocalHtCapable() ? &mib->getLocalHtCapabilities() : nullptr); + mib->publishStateChange(); } void Ieee80211MgmtStaSimplified::stop() { - mib->bssStationData.isAssociated = false; - auto apMib = findAccessPointMib(mib->bssData.bssid, false); + auto accessPointAddress = mib->getBssData().bssid; + mib->clearBss(); + auto apMib = findAccessPointMib(accessPointAddress, false); if (apMib != nullptr) { - apMib->bssAccessPointData.stations.erase(mib->address); - apMib->removePeerHtCapabilities(mib->address); + auto *apManagement = check_and_cast(apMib->getParentModule()->getSubmodule("mgmt")); + apManagement->removeSimplifiedPeer(mib->address); } Ieee80211MgmtBase::stop(); } diff --git a/src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.h b/src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.h new file mode 100644 index 00000000000..8ff9719e626 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.h @@ -0,0 +1,23 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef INET_IIEEE80211BSSPROVIDER_H +#define INET_IIEEE80211BSSPROVIDER_H +#include "inet/linklayer/common/MacAddress.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" +namespace inet::ieee80211 { +/** AP-owned preparation and relationship transitions for the simplified no-air model. */ +class INET_API IIeee80211BssProvider +{ + public: + virtual ~IIeee80211BssProvider() = default; + virtual void prepareBss() = 0; + virtual void installSimplifiedPeer(const MacAddress& address, const Ieee80211HtCapabilities *capabilities) = 0; + virtual void removeSimplifiedPeer(const MacAddress& address) = 0; +}; +} // namespace inet::ieee80211 +#endif diff --git a/src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.ned b/src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.ned new file mode 100644 index 00000000000..b35d80814d9 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.ned @@ -0,0 +1,12 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee80211.mgmt.contract; +// AP-owned preparation and relationship transitions for simplified management. +moduleinterface IIeee80211BssProvider +{ +} diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index 2174ce5c387..5d58918bc85 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -53,6 +53,15 @@ struct Ieee80211HtCapabilities bool shortGi20 = false; bool shortGi40 = false; int maxAmpduLengthExponent = 0; + + bool operator==(const Ieee80211HtCapabilities& other) const + { + return supportedChannelWidths == other.supportedChannelWidths && rxMcsSupported == other.rxMcsSupported && + txMcsSetDefined == other.txMcsSetDefined && txRxMcsSetNotEqual == other.txRxMcsSetNotEqual && + txMaxNss == other.txMaxNss && txUnequalModulation == other.txUnequalModulation && + txMcsNss.maxMcsPerNss == other.txMcsNss.maxMcsPerNss && ldpc == other.ldpc && greenfield == other.greenfield && + shortGi20 == other.shortGi20 && shortGi40 == other.shortGi40 && maxAmpduLengthExponent == other.maxAmpduLengthExponent; + } }; /** Model-backed subset of the HT Operation element (IEEE Std 802.11-2024, 9.4.2.55). */ @@ -63,6 +72,13 @@ struct Ieee80211HtOperation int secondaryChannelOffset = 0; Ieee80211HtProtectionMode protectionMode = Ieee80211HtProtectionMode::NO_PROTECTION; std::array basicMcsSupported = {}; + + bool operator==(const Ieee80211HtOperation& other) const + { + return operatingChannelWidth == other.operatingChannelWidth && primaryChannel == other.primaryChannel && + secondaryChannelOffset == other.secondaryChannelOffset && protectionMode == other.protectionMode && + basicMcsSupported == other.basicMcsSupported; + } }; struct Ieee80211HtDirectionalCapabilities @@ -83,16 +99,14 @@ struct Ieee80211NegotiatedHtCapabilities Ieee80211HtCapabilities peerAdvertisement; Ieee80211HtDirectionalCapabilities localTxPeerRx; Ieee80211HtDirectionalCapabilities localRxPeerTx; - Ieee80211HtOperation operation; }; inline Ieee80211NegotiatedHtCapabilities negotiateHtCapabilities(const Ieee80211HtCapabilities& local, - const Ieee80211HtCapabilities& peer, const Ieee80211HtOperation& operation) + const Ieee80211HtCapabilities& peer) { Ieee80211NegotiatedHtCapabilities negotiated; negotiated.localAdvertisement = local; negotiated.peerAdvertisement = peer; - negotiated.operation = operation; for (const auto& width : local.supportedChannelWidths) if (peer.supportedChannelWidths.count(width)) { negotiated.localTxPeerRx.supportedChannelWidths.insert(width); diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index e5c8fd008a3..bb147edcd19 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -18,16 +18,19 @@ namespace ieee80211 { Define_Module(Ieee80211Mib); +simsignal_t Ieee80211Mib::bssStateChangedSignal = cComponent::registerSignal("bssStateChanged"); + void Ieee80211Mib::initialize(int stage) { if (stage == INITSTAGE_LOCAL) { - configuredSecondaryChannelOffset = par("htSecondaryChannelOffset"); WATCH(address); WATCH(mode); WATCH(qos); WATCH(localHtCapabilitiesValid); - WATCH(configuredSecondaryChannelOffset); + WATCH(localCapabilitiesPrepared); WATCH(primaryChannelAvailable); + WATCH(bssActive); + WATCH(htOperationPresent); WATCH(bssData.bssid); WATCH(bssStationData.stationType); WATCH(bssStationData.isAssociated); @@ -44,183 +47,182 @@ void Ieee80211Mib::initialize(int stage) } } -int Ieee80211Mib::requirePrimaryChannel() const +void Ieee80211Mib::checkStateMutation() const { - if (!primaryChannelAvailable) - throw cRuntimeError("IEEE 802.11 primary channel is unavailable"); - return htOperation.primaryChannel; + if (publishingStateChange) + throw cRuntimeError("Cannot mutate IEEE 802.11 state during bssStateChanged notification"); } -void Ieee80211Mib::setPrimaryChannel(int primaryChannel) +void Ieee80211Mib::commitBss(const std::string& ssid, const MacAddress& bssid, const physicallayer::IIeee80211Band *band, + int channel, const Ieee80211HtOperation *operation) { - setPrimaryChannel(primaryChannel, nullptr); + checkStateMutation(); + if (channel < -1 || channel > 255 || (operation != nullptr && (channel < 0 || operation->primaryChannel != channel))) + throw cRuntimeError("Inconsistent IEEE 802.11 BSS channel snapshot"); + if (operation != nullptr && band != nullptr) + band->getStandardChannelNumber(channel); + bool changed = !bssActive || bssData.ssid != ssid || bssData.bssid != bssid || operationBand != band || + primaryChannelAvailable != (channel >= 0) || (channel >= 0 && htOperation.primaryChannel != channel) || + htOperationPresent != (operation != nullptr) || (operation != nullptr && !(htOperation == *operation)); + bssData.ssid = ssid; + bssData.bssid = bssid; + bssActive = true; + operationBand = band; + primaryChannelAvailable = channel >= 0; + htOperationPresent = operation != nullptr; + htOperation = operation != nullptr ? *operation : Ieee80211HtOperation(); + htOperation.primaryChannel = channel; + stateChangePending |= changed; } -void Ieee80211Mib::setPrimaryChannel(int primaryChannel, const physicallayer::IIeee80211Band *band) +void Ieee80211Mib::clearBss() { - if (primaryChannel < 0 || primaryChannel > 255) - throw cRuntimeError("IEEE 802.11 primary channel must be in the range 0..255, not %d", primaryChannel); - - if (band != nullptr) { - try { - band->getStandardChannelNumber(primaryChannel); - } - catch (const cRuntimeError&) { - throw cRuntimeError("Invalid primary channel %d for band '%s'", primaryChannel, band->getName()); - } + checkStateMutation(); + stateChangePending |= bssActive || !peerHtStates.empty() || !bssAccessPointData.stations.empty() || + !bssAccessPointData.associationIds.empty(); + bssActive = false; + htOperationPresent = false; + primaryChannelAvailable = false; + operationBand = nullptr; + bssStationData.isAssociated = false; + bssAccessPointData.stations.clear(); + bssAccessPointData.associationIds.clear(); + associationIdReservations.clear(); + peerHtStates.clear(); +} - if (localHtCapabilitiesValid) { - if (configuredSecondaryChannelOffset != 0) { - if (band->isHt40OperationSupported(primaryChannel, configuredSecondaryChannelOffset)) { - htOperation.secondaryChannelOffset = configuredSecondaryChannelOffset; - htOperation.operatingChannelWidth = MHz(40); - } - else { - // IEEE Std 802.11-2024, 11.15.2 and 11.15.3.1: fallback to 20 MHz BSS operation - EV_WARN << "Configured 40 MHz HT operation (offset " << configuredSecondaryChannelOffset - << ") is unsupported on primary channel " << primaryChannel - << " in band '" << band->getName() << "'; falling back to 20 MHz BSS operation.\n"; - htOperation.secondaryChannelOffset = 0; - htOperation.operatingChannelWidth = MHz(20); - } - } - else { - htOperation.secondaryChannelOffset = 0; - htOperation.operatingChannelWidth = MHz(20); - } - } +void Ieee80211Mib::publishStateChange() +{ + Enter_Method("publishStateChange"); + checkStateMutation(); + if (!stateChangePending) + return; + stateChangePending = false; + publishingStateChange = true; + try { + // No borrowed payload: observers query the committed MIB during this callback. + emit(bssStateChangedSignal, bssActive); } - - htOperation.primaryChannel = primaryChannel; - primaryChannelAvailable = true; - - if (localHtCapabilitiesValid) { - for (auto& entry : peerHtStates) { - if (entry.second.valid) { - entry.second.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, - entry.second.advertisedCapabilities, htOperation); - if (++entry.second.generation == 0) - entry.second.generation = 1; - } - } + catch (...) { + publishingStateChange = false; + throw; } + publishingStateChange = false; +} + +void Ieee80211Mib::configureBssRole(BssStationType stationType, const std::string& ssid) +{ + checkStateMutation(); + if (bssActive) + throw cRuntimeError("Cannot configure an active BSS role"); + bssStationData.stationType = stationType; + bssData.ssid = ssid; +} + +void Ieee80211Mib::setAssociated(bool associated) +{ + checkStateMutation(); + stateChangePending |= bssStationData.isAssociated != associated; + bssStationData.isAssociated = associated; +} + +Ieee80211Mib::BssMemberStatus Ieee80211Mib::getPeerAssociationStatus(const MacAddress& address) const +{ + auto it = bssAccessPointData.stations.find(address); + return it == bssAccessPointData.stations.end() ? NOT_AUTHENTICATED : it->second; +} + +void Ieee80211Mib::setPeerAssociationStatus(const MacAddress& address, BssMemberStatus status) +{ + checkStateMutation(); + auto it = bssAccessPointData.stations.find(address); + stateChangePending |= it == bssAccessPointData.stations.end() || it->second != status; + bssAccessPointData.stations[address] = status; +} + +void Ieee80211Mib::removePeerAssociation(const MacAddress& address) +{ + checkStateMutation(); + stateChangePending |= bssAccessPointData.stations.erase(address) != 0; + releaseAssociationId(address); +} + +int Ieee80211Mib::requirePrimaryChannel() const +{ + if (!primaryChannelAvailable) + throw cRuntimeError("IEEE 802.11 primary channel is unavailable"); + return htOperation.primaryChannel; } const Ieee80211HtOperation& Ieee80211Mib::getHtOperation() const { - requirePrimaryChannel(); + if (!hasHtOperation()) + throw cRuntimeError("No committed IEEE 802.11 HT operation is available"); return htOperation; } -void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, - const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit) +void Ieee80211Mib::installLocalHtCapabilities(const Ieee80211HtCapabilities& capabilities, bool htSupported) { - // The radio publishes its initial channel at PHYSICAL_LAYER before the MAC - // publishes its mode set at LINK_LAYER. Preserve that independent BSS - // operation input when rebuilding the mode-derived capability subset. - bool wasPrimaryChannelAvailable = primaryChannelAvailable; - int primaryChannel = htOperation.primaryChannel; - localHtCapabilities = Ieee80211HtCapabilities(); - htOperation = Ieee80211HtOperation(); - htOperation.primaryChannel = primaryChannel; - primaryChannelAvailable = wasPrimaryChannelAvailable; - localHtCapabilitiesValid = modeSet != nullptr && modeSet->isHtOperationSupported(); - if (!localHtCapabilitiesValid) { - clearPeerHtCapabilities(); + checkStateMutation(); + if (localCapabilitiesPrepared && localHtCapabilities == capabilities && localHtCapabilitiesValid == htSupported) return; - } - if (operationalHtSpatialStreamLimit <= 0) - throw cRuntimeError("HT operation requires a positive operational spatial-stream limit"); - - // IEEE Std 802.11-2024, 9.4.2.54.4 and 9.4.2.55: advertise exactly the - // HT modes come from the authoritative mode set, while advertised channel - // widths are restricted to those the configured transmitter and receiver - // can actually operate. In particular, do not infer dense MCS blocks or HT - // widths from legacy/VHT modes that happen to share the set. - const auto& mandatoryMcs = modeSet->getHtMcsMandatory(); - for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) - if (operationalChannelWidths.count(channelWidth) != 0) - localHtCapabilities.supportedChannelWidths.insert(channelWidth); - localHtCapabilities.shortGi20 = localHtCapabilities.supportedChannelWidths.count(MHz(20)) != 0 && - modeSet->isHtShortGuardIntervalSupported(MHz(20)); - localHtCapabilities.shortGi40 = localHtCapabilities.supportedChannelWidths.count(MHz(40)) != 0 && - modeSet->isHtShortGuardIntervalSupported(MHz(40)); - for (int index = 0; index < modeSet->getNumModes(); index++) { - const auto *mode = modeSet->getMode(index); - int mcs = mode->getHtMcsIndex(); - if (mcs >= 0 && mcs < 77 && operationalChannelWidths.count(mode->getDataMode()->getBandwidth()) != 0 && - mode->getDataMode()->getNumberOfSpatialStreams() <= operationalHtSpatialStreamLimit) - localHtCapabilities.rxMcsSupported[mcs] = true; - } - for (int mcs = 0; mcs < 77; mcs++) - htOperation.basicMcsSupported[mcs] = mandatoryMcs[mcs] && localHtCapabilities.rxMcsSupported[mcs]; - // The equal-case Tx MCS set is represented by the maximum MCS index per - // spatial-stream group. Rebuild it from the filtered Rx bitmap; MCS 32 is - // not part of this map's MCS 0..31 NSS encoding. - localHtCapabilities.txMcsNss = Ieee80211HtMcsNssMap(); - for (int mcs = 0; mcs < 32; mcs++) { - if (localHtCapabilities.rxMcsSupported[mcs]) { - int nss = mcs / 8; - localHtCapabilities.txMcsNss.maxMcsPerNss[nss] = std::max(localHtCapabilities.txMcsNss.maxMcsPerNss[nss], mcs % 8); - } - } - if (localHtCapabilities.supportedChannelWidths.empty()) - throw cRuntimeError("HT operation mode set '%s' does not provide an HT channel width", modeSet->getName()); - localHtCapabilities.maxAmpduLengthExponent = par("htMaxAmpduLengthExponent"); - if (localHtCapabilities.maxAmpduLengthExponent < 0 || localHtCapabilities.maxAmpduLengthExponent > 3) - throw cRuntimeError("htMaxAmpduLengthExponent must be between 0 and 3"); - - configuredSecondaryChannelOffset = par("htSecondaryChannelOffset"); - if (configuredSecondaryChannelOffset != 0 && configuredSecondaryChannelOffset != 1 && configuredSecondaryChannelOffset != 3) - throw cRuntimeError("htSecondaryChannelOffset must be 0, 1, or 3"); - htOperation.secondaryChannelOffset = configuredSecondaryChannelOffset; - bool use40Mhz = htOperation.secondaryChannelOffset != 0; - if (use40Mhz && localHtCapabilities.supportedChannelWidths.count(MHz(40)) == 0) - throw cRuntimeError("40 MHz HT operation requires a configured PHY that can operate a 40 MHz channel width"); - htOperation.operatingChannelWidth = use40Mhz ? MHz(40) : MHz(20); - int protectionMode = par("htProtectionMode"); - if (protectionMode < 0 || protectionMode > 3) - throw cRuntimeError("htProtectionMode must be between 0 and 3"); - htOperation.protectionMode = static_cast(protectionMode); - for (auto& entry : peerHtStates) { - if (entry.second.valid) { - entry.second.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, - entry.second.advertisedCapabilities, htOperation); - if (++entry.second.generation == 0) - entry.second.generation = 1; - } - } + if ((localCapabilitiesPrepared && bssActive) || !peerHtStates.empty()) + throw cRuntimeError("Cannot replace local HT capabilities with active BSS or peer relationships"); + localHtCapabilities = capabilities; + localHtCapabilitiesValid = htSupported; + localCapabilitiesPrepared = true; } -const Ieee80211Mib::PeerHtState *Ieee80211Mib::findPeerHtState(const MacAddress& address) const +const Ieee80211Mib::PeerHtState *Ieee80211Mib::findPeerCapabilities(const MacAddress& address) const { auto it = peerHtStates.find(address); return it == peerHtStates.end() || !it->second.valid ? nullptr : &it->second; } -void Ieee80211Mib::setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities, - const Ieee80211HtOperation& operation) +bool Ieee80211Mib::relationshipAllowsHt(const MacAddress& address) const +{ + const auto *peer = findPeerCapabilities(address); + if (!isLocalHtCapable() || !hasHtOperation() || peer == nullptr || !peer->negotiatedCapabilities) + return false; + const auto& capabilities = *peer->negotiatedCapabilities; + return capabilities.localTxPeerRx.valid && capabilities.localRxPeerTx.valid && + supportsBasicHtMcsSet(bssStationData.stationType == ACCESS_POINT ? peer->advertisedCapabilities : localHtCapabilities, htOperation); +} + +const Ieee80211Mib::PeerHtState *Ieee80211Mib::findPeerHtState(const MacAddress& address) const +{ + return relationshipAllowsHt(address) ? findPeerCapabilities(address) : nullptr; +} + +void Ieee80211Mib::setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities) { + checkStateMutation(); if (!localHtCapabilitiesValid) - throw cRuntimeError("Cannot install peer HT capabilities when local HT operation is disabled"); + throw cRuntimeError("Cannot install peer HT capabilities when local HT is disabled"); auto& state = peerHtStates[address]; - state.valid = true; + if (state.valid && state.advertisedCapabilities == capabilities && state.negotiatedCapabilities && + state.negotiatedCapabilities->localAdvertisement == localHtCapabilities) + return; + auto derived = std::make_shared(negotiateHtCapabilities(localHtCapabilities, capabilities)); state.advertisedCapabilities = capabilities; - state.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, capabilities, operation); - if (++state.generation == 0) - state.generation = 1; + state.negotiatedCapabilities = derived; + state.valid = true; + stateChangePending = true; EV_INFO << "Installed peer HT state, peer = " << address - << ", txValid = " << state.negotiatedCapabilities.localTxPeerRx.valid - << ", rxValid = " << state.negotiatedCapabilities.localRxPeerTx.valid << endl; + << ", txValid = " << derived->localTxPeerRx.valid + << ", rxValid = " << derived->localRxPeerTx.valid << endl; } void Ieee80211Mib::removePeerHtCapabilities(const MacAddress& address) { - peerHtStates.erase(address); + checkStateMutation(); + stateChangePending |= peerHtStates.erase(address) != 0; } void Ieee80211Mib::clearPeerHtCapabilities() { + checkStateMutation(); + stateChangePending |= !peerHtStates.empty(); peerHtStates.clear(); } @@ -252,6 +254,7 @@ const char *Ieee80211Mib::getStationTypeStr(Ieee80211Mib::BssStationType station short Ieee80211Mib::reserveAssociationId(const MacAddress& address) { + checkStateMutation(); // IEEE Std 802.11-2024, 9.4.1.8: an AP assigns AID values in the range 1 through 2007. auto committed = bssAccessPointData.associationIds.find(address); if (committed != bssAccessPointData.associationIds.end()) @@ -278,6 +281,7 @@ short Ieee80211Mib::reserveAssociationId(const MacAddress& address) short Ieee80211Mib::commitAssociationId(const MacAddress& address) { + checkStateMutation(); auto committed = bssAccessPointData.associationIds.find(address); if (committed != bssAccessPointData.associationIds.end()) { associationIdReservations.erase(address); @@ -291,12 +295,14 @@ short Ieee80211Mib::commitAssociationId(const MacAddress& address) if (entry.second == aid) throw cRuntimeError("Reserved IEEE 802.11 association ID %d is already committed", aid); bssAccessPointData.associationIds[address] = aid; + stateChangePending = true; associationIdReservations.erase(reserved); return aid; } void Ieee80211Mib::cancelAssociationIdReservation(const MacAddress& address) { + checkStateMutation(); associationIdReservations.erase(address); } @@ -308,13 +314,16 @@ short Ieee80211Mib::allocateAssociationId(const MacAddress& address) void Ieee80211Mib::releaseAssociationId(const MacAddress& address) { + checkStateMutation(); associationIdReservations.erase(address); - bssAccessPointData.associationIds.erase(address); + stateChangePending |= bssAccessPointData.associationIds.erase(address) != 0; removePeerHtCapabilities(address); } void Ieee80211Mib::clearAssociationIds() { + checkStateMutation(); + stateChangePending |= !bssAccessPointData.stations.empty() || !bssAccessPointData.associationIds.empty(); bssAccessPointData.stations.clear(); associationIdReservations.clear(); bssAccessPointData.associationIds.clear(); diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index 691257c2f07..c5622a85a02 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211MIB_H #define __INET_IEEE80211MIB_H +#include + #include "inet/common/SimpleModule.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" @@ -24,6 +26,8 @@ namespace ieee80211 { class INET_API Ieee80211Mib : public SimpleModule { public: + static simsignal_t bssStateChangedSignal; + enum Mode { INFRASTRUCTURE, INDEPENDENT, @@ -63,8 +67,7 @@ class INET_API Ieee80211Mib : public SimpleModule public: bool valid = false; Ieee80211HtCapabilities advertisedCapabilities; - Ieee80211NegotiatedHtCapabilities negotiatedCapabilities; - uint64_t generation = 0; + std::shared_ptr negotiatedCapabilities; }; public: @@ -72,6 +75,7 @@ class INET_API Ieee80211Mib : public SimpleModule Mode mode = static_cast(-1); bool qos = false; + private: BssData bssData; BssStationData bssStationData; BssAccessPointData bssAccessPointData; @@ -82,8 +86,15 @@ class INET_API Ieee80211Mib : public SimpleModule private: Ieee80211HtOperation htOperation; - int configuredSecondaryChannelOffset = 0; + bool localCapabilitiesPrepared = false; bool primaryChannelAvailable = false; + bool bssActive = false; + bool htOperationPresent = false; + const physicallayer::IIeee80211Band *operationBand = nullptr; + bool stateChangePending = false; + bool publishingStateChange = false; + + void checkStateMutation() const; std::map associationIdReservations; std::map peerHtStates; @@ -91,6 +102,15 @@ class INET_API Ieee80211Mib : public SimpleModule virtual void initialize(int stage) override; public: + const BssData& getBssData() const { return bssData; } + const BssStationData& getBssStationData() const { return bssStationData; } + const BssAccessPointData& getBssAccessPointData() const { return bssAccessPointData; } + const Ieee80211HtCapabilities& getLocalHtCapabilities() const { return localHtCapabilities; } + void configureBssRole(BssStationType stationType, const std::string& ssid = ""); + void setAssociated(bool associated); + BssMemberStatus getPeerAssociationStatus(const MacAddress& address) const; + void setPeerAssociationStatus(const MacAddress& address, BssMemberStatus status); + void removePeerAssociation(const MacAddress& address); static const char *getModeStr(Ieee80211Mib::Mode mode); static const char *getStationTypeStr(Ieee80211Mib::BssStationType stationType); std::string getSsidStr() const; @@ -100,16 +120,26 @@ class INET_API Ieee80211Mib : public SimpleModule short allocateAssociationId(const MacAddress& address); void releaseAssociationId(const MacAddress& address); void clearAssociationIds(); - void updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, - const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit); - bool isHtOperationSupported() const { return localHtCapabilitiesValid; } + // Initialization/preparation only. A changed profile requires inactive BSS and no peers. + void installLocalHtCapabilities(const Ieee80211HtCapabilities& capabilities, bool htSupported); + bool hasPreparedLocalCapabilities() const { return localCapabilitiesPrepared; } + bool isLocalHtCapable() const { return localHtCapabilitiesValid; } + bool hasActiveBss() const { return bssActive; } + bool hasHtOperation() const { return bssActive && htOperationPresent; } + const physicallayer::IIeee80211Band *getOperationBand() const { return operationBand; } + void commitBss(const std::string& ssid, const MacAddress& bssid, const physicallayer::IIeee80211Band *band, + int channel, const Ieee80211HtOperation *operation); + void clearBss(); + // Management publishes only after its required transaction/timer bookkeeping. + // Synchronous observers may query state; nested mutation is rejected. + void publishStateChange(); bool hasPrimaryChannel() const { return primaryChannelAvailable; } int requirePrimaryChannel() const; - void setPrimaryChannel(int primaryChannel); - void setPrimaryChannel(int primaryChannel, const physicallayer::IIeee80211Band *band); const Ieee80211HtOperation& getHtOperation() const; + bool relationshipAllowsHt(const MacAddress& address) const; + const PeerHtState *findPeerCapabilities(const MacAddress& address) const; const PeerHtState *findPeerHtState(const MacAddress& address) const; - void setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities, const Ieee80211HtOperation& operation); + void setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities); void removePeerHtCapabilities(const MacAddress& address); void clearPeerHtCapabilities(); }; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index 70474b3d95b..ac929589c40 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -20,6 +20,7 @@ simple Ieee80211Mib extends SimpleModule { parameters: @class(Ieee80211Mib); + @signal[bssStateChanged](type=bool); // committed active state; synchronous read-only observation // Model-backed subset of IEEE Std 802.11-2024 HT capability/operation state; not a full Annex C MIB. int htMaxAmpduLengthExponent = default(0); // maximum received A-MPDU length exponent (0..3) int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; bounded by the mode set diff --git a/src/inet/networklayer/configurator/base/L3NetworkConfiguratorBase.cc b/src/inet/networklayer/configurator/base/L3NetworkConfiguratorBase.cc index c8fbb5d406e..0b81bbedc0b 100644 --- a/src/inet/networklayer/configurator/base/L3NetworkConfiguratorBase.cc +++ b/src/inet/networklayer/configurator/base/L3NetworkConfiguratorBase.cc @@ -509,7 +509,7 @@ std::string L3NetworkConfiguratorBase::getWirelessId(NetworkInterface *networkIn #endif // INET_WITH_PHYSICALLAYERWIRELESSCOMMON #ifdef INET_WITH_IEEE80211 if (auto mibModule = dynamic_cast(interfaceModule->getSubmodule("mib"))) { - auto ssid = mibModule->bssData.ssid; + auto ssid = mibModule->getBssData().ssid; if (ssid.length() != 0) return mediumName + ":" + ssid; } diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.h b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.h new file mode 100644 index 00000000000..d3d0c1fcedc --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.h @@ -0,0 +1,25 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef INET_IIEEE80211RECEIVERCAPABILITIES_H +#define INET_IIEEE80211RECEIVERCAPABILITIES_H + +#include "inet/common/Units.h" + +namespace inet::physicallayer { + +/** Read-only implemented HT abilities, ready after physical-layer initialization. + * False means unsupported, independently of the selected BSS operation. + */ +class INET_API IIeee80211ReceiverCapabilities +{ + public: + virtual ~IIeee80211ReceiverCapabilities() = default; + [[nodiscard]] virtual bool isHtChannelWidthSupported(Hz channelWidth) const = 0; + [[nodiscard]] virtual bool isHtShortGuardIntervalSupported(Hz channelWidth) const = 0; +}; + +} // namespace inet::physicallayer + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.ned b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.ned new file mode 100644 index 00000000000..684d0bae75f --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.ned @@ -0,0 +1,10 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +package inet.physicallayer.wireless.ieee80211.contract; + +// Implemented HT abilities queried through the matching C++ contract. +moduleinterface IIeee80211ReceiverCapabilities +{ +} diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.h b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.h new file mode 100644 index 00000000000..7e1856a044e --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.h @@ -0,0 +1,24 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef INET_IIEEE80211TRANSMITTERCAPABILITIES_H +#define INET_IIEEE80211TRANSMITTERCAPABILITIES_H + +#include "inet/common/Units.h" + +namespace inet::physicallayer { + +/** Read-only implemented HT abilities, ready after physical-layer initialization. + * False means unsupported, independently of the selected BSS operation. + */ +class INET_API IIeee80211TransmitterCapabilities +{ + public: + virtual ~IIeee80211TransmitterCapabilities() = default; + [[nodiscard]] virtual bool isHtChannelWidthSupported(Hz channelWidth) const = 0; +}; + +} // namespace inet::physicallayer + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.ned b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.ned new file mode 100644 index 00000000000..ffbf8febd2e --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.ned @@ -0,0 +1,10 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +package inet.physicallayer.wireless.ieee80211.contract; + +// Implemented HT abilities queried through the matching C++ contract. +moduleinterface IIeee80211TransmitterCapabilities +{ +} diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 4c9d2ecd6d1..7609deade75 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -99,6 +99,11 @@ void Ieee80211Receiver::setChannelNumber(int channelNumber) setChannel(new Ieee80211Channel(band, channelNumber)); } +bool Ieee80211Receiver::isHtShortGuardIntervalSupported(Hz channelWidth) const +{ + return isHtChannelWidthSupported(channelWidth) && modeSet->isHtShortGuardIntervalSupported(channelWidth); +} + bool Ieee80211Receiver::isHtChannelWidthSupported(Hz channelWidth) const { // The receiver listens around the primary-channel center and cannot yet diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h index 450e8b36c45..d71d16deee1 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -9,6 +9,7 @@ #define __INET_IEEE80211RECEIVER_H #include "inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h" @@ -17,7 +18,7 @@ namespace inet { namespace physicallayer { -class INET_API Ieee80211Receiver : public FlatReceiverBase +class INET_API Ieee80211Receiver : public FlatReceiverBase, public IIeee80211ReceiverCapabilities { protected: const Ieee80211ModeSet *modeSet = nullptr; @@ -41,7 +42,8 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); virtual void setChannelNumber(int channelNumber); - virtual bool isHtChannelWidthSupported(Hz channelWidth) const; + bool isHtChannelWidthSupported(Hz channelWidth) const override; + bool isHtShortGuardIntervalSupported(Hz channelWidth) const override; }; } // namespace physicallayer diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned index c0c19e2afc2..3d1a0bc720d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned @@ -7,6 +7,8 @@ package inet.physicallayer.wireless.ieee80211.packetlevel; +import inet.physicallayer.wireless.ieee80211.contract.IIeee80211ReceiverCapabilities; + import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandReceiverBase; @@ -20,7 +22,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandReceiverBas // @see ~Ieee80211Transmitter, ~Ieee80211ScalarRadio, // ~Ieee80211ScalarRadioMedium. // -module Ieee80211Receiver extends NarrowbandReceiverBase +module Ieee80211Receiver extends NarrowbandReceiverBase like IIeee80211ReceiverCapabilities { parameters: string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index 05b6f29194c..354a42153bd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -9,6 +9,7 @@ #define __INET_IEEE80211TRANSMITTER_H #include "inet/physicallayer/wireless/common/base/packetlevel/FlatTransmitterBase.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.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" @@ -19,7 +20,7 @@ namespace inet { namespace physicallayer { -class INET_API Ieee80211Transmitter : public FlatTransmitterBase +class INET_API Ieee80211Transmitter : public FlatTransmitterBase, public IIeee80211TransmitterCapabilities { protected: const Ieee80211ModeSet *modeSet = nullptr; @@ -45,7 +46,7 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase virtual void setChannelNumber(int channelNumber); virtual const Ieee80211Channel *getChannel() const { return channel; } - virtual bool isHtChannelWidthSupported(Hz channelWidth) const; + virtual bool isHtChannelWidthSupported(Hz channelWidth) const override; virtual const ITransmission *createTransmission(const IRadio *radio, const Packet *packet, simtime_t startTime) const override; }; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned index 3d50ab670e5..7d5cedb7505 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned @@ -7,6 +7,8 @@ package inet.physicallayer.wireless.ieee80211.packetlevel; +import inet.physicallayer.wireless.ieee80211.contract.IIeee80211TransmitterCapabilities; + import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandTransmitterBase; // @@ -14,7 +16,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandTransmitter // // @see ~Ieee80211Receiver, ~Ieee80211Radio, ~Ieee80211RadioMedium. // -module Ieee80211Transmitter extends NarrowbandTransmitterBase +module Ieee80211Transmitter extends NarrowbandTransmitterBase like IIeee80211TransmitterCapabilities { parameters: string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); diff --git a/tests/module/Ieee80211AgentStaReassociation_1.test b/tests/module/Ieee80211AgentStaReassociation_1.test index 2462b058ff6..283ee059dde 100644 --- a/tests/module/Ieee80211AgentStaReassociation_1.test +++ b/tests/module/Ieee80211AgentStaReassociation_1.test @@ -73,11 +73,11 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta void setAssociated(const MacAddress& address) { Enter_Method("setAssociated"); - if (mib->bssStationData.isAssociated) + if (mib->getBssStationData().isAssociated) clearCurrentAssociation(); ensureAccessPoint(address); - mib->bssData.bssid = address; - mib->bssStationData.isAssociated = true; + mib->commitBss("SSID", address, nullptr, -1, nullptr); + mib->setAssociated(true); assocAP = AssociatedApInfo(); assocAP.address = address; assocAP.channel = 1; @@ -99,7 +99,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta void beginPendingReassociation(const MacAddress& targetAddress) { Enter_Method("beginPendingReassociation"); - ASSERT(mib->bssStationData.isAssociated); + ASSERT(mib->getBssStationData().isAssociated); ASSERT(assocTimeoutMsg == nullptr); auto target = ensureAccessPoint(targetAddress); assocTimeoutMsg = new cMessage("assocTimeout", 2); @@ -127,7 +127,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta processAssociationResponse(packet, header, true); } - bool isAssociated() const { return mib->bssStationData.isAssociated; } + bool isAssociated() const { return mib->getBssStationData().isAssociated; } MacAddress getAssociatedAddress() const { return assocAP.address; } bool hasBeaconTimer() const { return assocAP.beaconTimeoutMsg != nullptr; } bool hasPendingReassociation() const { return assocTimeoutMsg != nullptr && reassociationInProgress; } @@ -182,7 +182,7 @@ class CompletionListener : public cListener acceptSeen = true; acceptCount++; events.push_back("accept"); - callbackStateValid = callbackStateValid && agent->getPreviousAp() == expectedAddress && mib->bssStationData.isAssociated; + callbackStateValid = callbackStateValid && agent->getPreviousAp() == expectedAddress && mib->getBssStationData().isAssociated; } virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *value, cObject *details) override @@ -197,7 +197,7 @@ class CompletionListener : public cListener } else return; - callbackStateValid = callbackStateValid && acceptSeen && agent->getPreviousAp() == expectedAddress && mib->bssStationData.isAssociated; + callbackStateValid = callbackStateValid && acceptSeen && agent->getPreviousAp() == expectedAddress && mib->getBssStationData().isAssociated; } }; diff --git a/tests/module/Ieee80211ConfigurationContracts_1.test b/tests/module/Ieee80211ConfigurationContracts_1.test new file mode 100644 index 00000000000..5b59c0fa6e7 --- /dev/null +++ b/tests/module/Ieee80211ConfigurationContracts_1.test @@ -0,0 +1,202 @@ +%description: +Explicit catalog dependency with a replacement MAC provider outside any interface, +missing-provider diagnostics, and prepared legacy/HT ad hoc lifecycle state. +The external-upper composition replaces only its TAP endpoint before initialization; +no external OS device is opened and no real-time packet I/O is claimed. + +%file: ConfigurationContracts.cc +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/emulation/common/ExtInterface.h" +#include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class OfflineTap : public cSimpleModule +{ + protected: + virtual void handleMessage(cMessage *message) override { delete message; } +}; +Define_Module(OfflineTap); + +// Keep the production external-upper NED, interface class, MAC, management, +// PHY and parameter routing. Replace only the OS endpoint before child init. +class OfflineExtUpper : public ExtInterface +{ + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) { + auto *tap = getSubmodule("tap"); + auto *fromLlc = tap->gate("lowerLayerIn")->getPreviousGate(); + auto *toClassifier = tap->gate("lowerLayerOut")->getNextGate(); + fromLlc->disconnect(); + tap->gate("lowerLayerOut")->disconnect(); + tap->deleteModule(); + auto *offline = cModuleType::get("OfflineTap")->create("tap", this); + offline->finalizeParameters(); + offline->buildInside(); + fromLlc->connectTo(offline->gate("lowerLayerIn")); + offline->gate("lowerLayerOut")->connectTo(toClassifier); + } + ExtInterface::initialize(stage); + } +}; +Define_Module(OfflineExtUpper); + +class TestCatalogProvider : public cSimpleModule, public IIeee80211MacConfiguration +{ + protected: + const Ieee80211ModeSet *catalog = nullptr; + virtual void initialize() override { catalog = Ieee80211ModeSet::getModeSet("g(mixed)"); } + public: + virtual const Ieee80211ModeSet *getConfiguredModeSet() const override { return catalog; } + virtual void prepareLocalCapabilities() override { throw cRuntimeError("Catalog-only consumer must not prepare HT"); } +}; +Define_Module(TestCatalogProvider); + +class TestCatalogConsumer : public ModeSetModuleBase +{ + protected: + virtual void initialize(int stage) override + { + if (par("missing")) { + if (stage == INITSTAGE_LOCAL) { + bool rejected = false; + try { ModeSetModuleBase::initialize(stage); } + catch (const cRuntimeError& e) { rejected = std::string(e.what()).find("absent") != std::string::npos; } + ASSERT(rejected); + std::cout << "Missing explicit catalog provider rejected.\n"; + } + return; + } + ModeSetModuleBase::initialize(stage); + if (stage == INITSTAGE_LINK_LAYER) { + ASSERT(modeSet == Ieee80211ModeSet::getModeSet("g(mixed)")); + std::cout << "Standalone consumer queried replacement provider after LOCAL.\n"; + } + } +}; +Define_Module(TestCatalogConsumer); + +class TestAdhocState : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_NETWORK_CONFIGURATION) { + check(true); + auto *rateControl = check_and_cast(getModuleByPath("^.ht.wlan[0].mac.dcf.rateControl")); + auto *rate = rateControl->getRate(MacAddress("02:00:00:00:00:09")); + ASSERT(rate->getDataMode()->getNetBitrate() == Mbps(65)); + std::cout << "Onoe initial rate is prepared before first network-configuration query.\n"; + auto *external = check_and_cast(getModuleByPath("^.external.wlan[0].mib")); + ASSERT(external->hasPreparedLocalCapabilities() && external->isLocalHtCapable()); + ASSERT(external->hasActiveBss() && !external->hasHtOperation()); + std::cout << "External-upper production dependency routing prepared with an offline TAP endpoint.\n"; + scheduleAt(SimTime(1500, SIMTIME_NS), new cMessage("down")); + scheduleAt(SimTime(2500, SIMTIME_NS), new cMessage("restarted")); + } + } + void check(bool active) + { + for (const char *name : {"^.legacy.wlan[0].mib", "^.ht.wlan[0].mib"}) { + auto *mib = check_and_cast(getModuleByPath(name)); + ASSERT(mib->hasPreparedLocalCapabilities()); + ASSERT(mib->isLocalHtCapable() == (std::string(name).find(".ht.") != std::string::npos)); + ASSERT(mib->hasActiveBss() == active); + ASSERT(!mib->hasHtOperation()); + ASSERT(!mib->hasPrimaryChannel()); + } + } + virtual void handleMessage(cMessage *message) override + { + bool restarted = std::string(message->getName()) == "restarted"; + check(restarted); + if (restarted) + std::cout << "Legacy and HT ad hoc state cleared and restored without fabricated HT operation.\n"; + delete message; + } +}; +Define_Module(TestAdhocState); + +%file: test.ned +import inet.common.SimpleModule; +import inet.emulation.linklayer.ieee80211.ExtUpperIeee80211Interface; +import inet.common.scenario.ScenarioManager; +import inet.node.inet.AdhocHost; +import inet.linklayer.ieee80211.mac.contract.IIeee80211MacConfiguration; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple OfflineTap { parameters: @class(::OfflineTap); gates: input lowerLayerIn; output lowerLayerOut; } +module OfflineExtUpper extends ExtUpperIeee80211Interface { parameters: @class(::OfflineExtUpper); } +simple TestCatalogProvider like IIeee80211MacConfiguration { parameters: @class(::TestCatalogProvider); } +simple TestCatalogConsumer extends SimpleModule { + parameters: + @class(::TestCatalogConsumer); + string modeSetModule; + bool missing = default(false); +} +simple TestAdhocState extends SimpleModule { parameters: @class(::TestAdhocState); } +network ConfigurationContracts { + submodules: + consumer: TestCatalogConsumer { parameters: modeSetModule = "^.provider"; } + missing: TestCatalogConsumer { parameters: modeSetModule = "^.absent"; missing = true; } + provider: TestCatalogProvider; + radioMedium: Ieee80211ScalarRadioMedium; + scenarioManager: ScenarioManager; + legacy: AdhocHost; + ht: AdhocHost; + external: AdhocHost; + observer: TestAdhocState; +} + +%inifile: omnetpp.ini +[General] +network = ConfigurationContracts +ned-path = .;../../../../src;../../lib +sim-time-limit = 3us +seed-set = 0 +record-vector-results = false +record-scalar-results = false +**.hasStatus = true +**.ipv4.configurator.networkConfiguratorModule = "" +*.external.wlan[0].typename = "OfflineExtUpper" +*.external.wlan[0].device = "offline-test" +*.external.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.external.wlan[0].mgmt.typename = "Ieee80211MgmtAdhoc" +*.external.wlan[0].agent.typename = "" +*.ht.wlan[0].mac.dcf.rateControl.typename = "OnoeRateControl" +*.ht.wlan[0].mac.dcf.rateControl.initialRate = 65Mbps +*.ht.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.legacy.wlan[0].opMode = "g(mixed)" +**.radio.bandName = "2.4 GHz" +**.radio.channelNumber = 6 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.scenarioManager.script = xmldoc("scenario.xml") + +%file: scenario.xml + + + + + +%contains: stdout +Missing explicit catalog provider rejected. +%contains: stdout +Standalone consumer queried replacement provider after LOCAL. +%contains: stdout +Legacy and HT ad hoc state cleared and restored without fabricated HT operation. + +%contains: stdout +External-upper production dependency routing prepared with an offline TAP endpoint. + +%contains: stdout +Onoe initial rate is prepared before first network-configuration query. diff --git a/tests/module/Ieee80211HtAntennaRateControl_1.test b/tests/module/Ieee80211HtAntennaRateControl_1.test index 0518558627c..d064c84efe2 100644 --- a/tests/module/Ieee80211HtAntennaRateControl_1.test +++ b/tests/module/Ieee80211HtAntennaRateControl_1.test @@ -101,12 +101,12 @@ class Ieee80211HtAntennaRateControlTest : public cSimpleModule auto staMib = check_and_cast(staInterface->getSubmodule("mib")); auto apMib = check_and_cast(apInterface->getSubmodule("mib")); - ASSERT(staMib->bssStationData.isAssociated); - ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == Ieee80211Mib::ASSOCIATED); - ASSERT(staMib->localHtCapabilitiesValid); - ASSERT(apMib->localHtCapabilitiesValid); - checkCapabilities(staMib->localHtCapabilities); - checkCapabilities(apMib->localHtCapabilities); + ASSERT(staMib->getBssStationData().isAssociated); + ASSERT(apMib->getBssAccessPointData().stations.at(staMib->address) == Ieee80211Mib::ASSOCIATED); + ASSERT(staMib->isLocalHtCapable()); + ASSERT(apMib->isLocalHtCapable()); + checkCapabilities(staMib->getLocalHtCapabilities()); + checkCapabilities(apMib->getLocalHtCapabilities()); checkBasicMcs(apMib->getHtOperation()); auto staPeer = staMib->findPeerHtState(apMib->address); @@ -114,14 +114,14 @@ class Ieee80211HtAntennaRateControlTest : public cSimpleModule ASSERT(staPeer != nullptr); ASSERT(apPeer != nullptr); checkCapabilities(staPeer->advertisedCapabilities); - checkBasicMcs(staPeer->negotiatedCapabilities.operation); + checkBasicMcs(staMib->getHtOperation()); checkCapabilities(apPeer->advertisedCapabilities); - checkBasicMcs(apPeer->negotiatedCapabilities.operation); + checkBasicMcs(apMib->getHtOperation()); for (int mcs = 8; mcs < 77; mcs++) { - ASSERT(!staPeer->negotiatedCapabilities.localTxPeerRx.supportedMcs[mcs]); - ASSERT(!staPeer->negotiatedCapabilities.localRxPeerTx.supportedMcs[mcs]); - ASSERT(!apPeer->negotiatedCapabilities.localTxPeerRx.supportedMcs[mcs]); - ASSERT(!apPeer->negotiatedCapabilities.localRxPeerTx.supportedMcs[mcs]); + ASSERT(!staPeer->negotiatedCapabilities->localTxPeerRx.supportedMcs[mcs]); + ASSERT(!staPeer->negotiatedCapabilities->localRxPeerTx.supportedMcs[mcs]); + ASSERT(!apPeer->negotiatedCapabilities->localTxPeerRx.supportedMcs[mcs]); + ASSERT(!apPeer->negotiatedCapabilities->localRxPeerTx.supportedMcs[mcs]); } auto rateControl = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); diff --git a/tests/module/Ieee80211HtAssociation_1.test b/tests/module/Ieee80211HtAssociation_1.test index aeee34722e2..c7b9271445b 100644 --- a/tests/module/Ieee80211HtAssociation_1.test +++ b/tests/module/Ieee80211HtAssociation_1.test @@ -120,27 +120,27 @@ class Ieee80211HtAssociationChecker : public SimpleModule, public cListener const auto& staAddress = staMib->address; ASSERT(apMib->requirePrimaryChannel() == 11); ASSERT(channelChangeTimer == nullptr); - ASSERT(apMib->bssAccessPointData.stations.at(staAddress) == Ieee80211Mib::ASSOCIATED); - ASSERT(apMib->bssAccessPointData.associationIds.at(staAddress) != 0); + ASSERT(apMib->getBssAccessPointData().stations.at(staAddress) == Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->getBssAccessPointData().associationIds.at(staAddress) != 0); ASSERT(apMib->findPeerHtState(staAddress) != nullptr); ASSERT(apMib->findPeerHtState(staAddress)->valid); ASSERT(associationResponseWireChannelChecked); ASSERT(staTunedToInternalChannelSix); ASSERT(apMib->hasPrimaryChannel()); ASSERT(apMib->requirePrimaryChannel() == 11); - ASSERT(apMib->findPeerHtState(staAddress)->negotiatedCapabilities.operation.primaryChannel == 11); - ASSERT(staMib->bssStationData.isAssociated); + ASSERT(apMib->getHtOperation().primaryChannel == 11); + ASSERT(staMib->getBssStationData().isAssociated); // IEEE Std 802.11-2024, 9.4.2.54.2/Figure 9-456 and Table 9-224: the n(mixed-2.4Ghz) // profile exposes short GI for the PHY-supported 20 MHz width. The // mode catalog also describes 40 MHz modes, but the current packet PHY // cannot operate a primary/secondary compound channel and must not // advertise that width. - ASSERT(staMib->localHtCapabilities.shortGi20); - ASSERT(!staMib->localHtCapabilities.shortGi40); - ASSERT(apMib->localHtCapabilities.shortGi20); - ASSERT(!apMib->localHtCapabilities.shortGi40); - ASSERT(staMib->localHtCapabilities.supportedChannelWidths.count(MHz(20)) == 1); - ASSERT(staMib->localHtCapabilities.supportedChannelWidths.count(MHz(40)) == 0); + ASSERT(staMib->getLocalHtCapabilities().shortGi20); + ASSERT(!staMib->getLocalHtCapabilities().shortGi40); + ASSERT(apMib->getLocalHtCapabilities().shortGi20); + ASSERT(!apMib->getLocalHtCapabilities().shortGi40); + ASSERT(staMib->getLocalHtCapabilities().supportedChannelWidths.count(MHz(20)) == 1); + ASSERT(staMib->getLocalHtCapabilities().supportedChannelWidths.count(MHz(40)) == 0); // Bridge the MIB-derived capability contract to the actual management // frame serializer: the n(mixed-2.4Ghz) profile must advertise only // PHY-supported short-GI bits in HT Capabilities (IEEE Std 802.11-2024, 9.4.2.54.2, @@ -152,7 +152,7 @@ class Ieee80211HtAssociationChecker : public SimpleModule, public cListener rates.numRates = 1; rates.rate[0] = 6; serializedResponse->setSupportedRates(rates); - setHtCapabilities(serializedResponse, staMib->localHtCapabilities); + setHtCapabilities(serializedResponse, staMib->getLocalHtCapabilities()); serializedResponse->setChunkLength(B(37)); // fixed fields/rates (9) + HT Capabilities IE (28) Packet serializedPacket("mib-ht-capabilities", serializedResponse); auto serializedBytes = serializedPacket.peekAllAsBytes()->getBytes(); @@ -162,7 +162,7 @@ class Ieee80211HtAssociationChecker : public SimpleModule, public cListener ASSERT((serializedBytes[11] & (1 << 6)) == 0); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address)->valid); - ASSERT(staMib->findPeerHtState(apMib->address)->negotiatedCapabilities.operation.primaryChannel == 6); + ASSERT(staMib->getHtOperation().primaryChannel == 6); ASSERT(apAssociationNotifications == 1); EV << "RTS-protected association committed at AP and STA.\n"; } diff --git a/tests/module/Ieee80211HtCapabilityPreparation_1.test b/tests/module/Ieee80211HtCapabilityPreparation_1.test new file mode 100644 index 00000000000..8cb8c6327a7 --- /dev/null +++ b/tests/module/Ieee80211HtCapabilityPreparation_1.test @@ -0,0 +1,157 @@ +%description: +The production MAC assembles HT abilities through replaceable PHY query contracts. +A transmitter adapter unrelated to Ieee80211Transmitter contributes different widths; +receiver short GI remains an independent contribution. Capability installation and +repeated preparation preserve operation. This is synthetic initialization coverage, +not a 40 MHz PHY exchange. + +%file: TestHtPreparation.cc +#include "inet/common/SimpleModule.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class ContributionTransmitter : public ITransmitter, public IIeee80211TransmitterCapabilities +{ + public: + const ITransmitter *delegate = nullptr; + bool allow40 = false; + virtual W getMaxPower() const override { return delegate->getMaxPower(); } + virtual m getMaxCommunicationRange() const override { return delegate->getMaxCommunicationRange(); } + virtual m getMaxInterferenceRange() const override { return delegate->getMaxInterferenceRange(); } + virtual const ITransmission *createTransmission(const IRadio *radio, const Packet *packet, simtime_t time) const override + { return delegate->createTransmission(radio, packet, time); } + virtual bool isHtChannelWidthSupported(Hz width) const override + { return width == MHz(20) || (allow40 && width == MHz(40)); } +}; + +class ContributionRadio : public Ieee80211Radio +{ + ContributionTransmitter contribution; + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) { + contribution.delegate = transmitter; + contribution.allow40 = par("allow40"); + } + } + public: + virtual const ITransmitter *getTransmitter() const override { return &contribution; } +}; +Define_Module(ContributionRadio); + +class ContributionReceiver : public Ieee80211Receiver +{ + public: + virtual bool isHtChannelWidthSupported(Hz width) const override + { return width == MHz(20) || width == MHz(40); } + virtual bool isHtShortGuardIntervalSupported(Hz width) const override + { return width == MHz(20); } +}; +Define_Module(ContributionReceiver); + +class HtPreparationChecker : public SimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + for (const char *name : {"^.narrow.wlan[0]", "^.wide.wlan[0]"}) { + auto *nic = getModuleByPath(name); + auto *mib = check_and_cast(nic->getSubmodule("mib")); + auto *mac = check_and_cast(nic->getSubmodule("mac")); + ASSERT(mib->hasPreparedLocalCapabilities()); + ASSERT(mib->getLocalHtCapabilities().supportedChannelWidths.count(MHz(20)) == 1); + ASSERT(mib->getLocalHtCapabilities().shortGi20); + ASSERT(!mib->getLocalHtCapabilities().shortGi40); + bool wide = std::string(name).find("wide") != std::string::npos; + ASSERT(mib->getLocalHtCapabilities().supportedChannelWidths.count(MHz(40)) == (wide ? 1 : 0)); + auto operation = mib->getHtOperation(); + mac->prepareLocalCapabilities(); + ASSERT(mib->getHtOperation().primaryChannel == operation.primaryChannel); + ASSERT(mib->getHtOperation().basicMcsSupported == operation.basicMcsSupported); + } + auto *storage = check_and_cast(getModuleByPath("^.storage")); + Ieee80211HtOperation operation; + operation.primaryChannel = 6; + operation.protectionMode = Ieee80211HtProtectionMode::NON_HT_MIXED; + operation.basicMcsSupported[2] = true; + storage->commitBss("storage", MacAddress::UNSPECIFIED_ADDRESS, nullptr, 6, &operation); + Ieee80211HtCapabilities capabilities; + capabilities.supportedChannelWidths.insert(MHz(20)); + capabilities.rxMcsSupported[0] = true; + storage->installLocalHtCapabilities(capabilities, true); + ASSERT(storage->getHtOperation().primaryChannel == 6); + ASSERT(storage->getHtOperation().protectionMode == operation.protectionMode); + ASSERT(storage->getHtOperation().basicMcsSupported == operation.basicMcsSupported); + std::cout << "Independent capability preparation and replacement contributions verified.\n"; + } +}; +Define_Module(HtPreparationChecker); + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mib.Ieee80211Mib; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Receiver; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module ContributionRadio extends Ieee80211ScalarRadio { + parameters: + bool allow40 = default(false); + @class(::ContributionRadio); +} +module ContributionReceiver extends Ieee80211Receiver { + parameters: + @class(::ContributionReceiver); +} +simple HtPreparationChecker extends SimpleModule { + parameters: + @class(::HtPreparationChecker); +} +network HtPreparationNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + narrow: AccessPoint; + wide: AccessPoint; + storage: Ieee80211Mib; + checker: HtPreparationChecker; +} + +%inifile: omnetpp.ini +[General] +network = HtPreparationNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 0s +seed-set = 0 +record-vector-results = false +record-scalar-results = false +**.wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].radio.typename = "ContributionRadio" +**.wlan[*].radio.receiver.typename = "ContributionReceiver" +*.wide.wlan[*].radio.allow40 = true +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 6 +**.wlan[*].bitrate = 65Mbps +**.wlan[*].radio.antenna.numAntennas = 1 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m + +%contains: stdout +Independent capability preparation and replacement contributions verified. diff --git a/tests/module/Ieee80211MgmtApChannelChange_1.test b/tests/module/Ieee80211MgmtApChannelChange_1.test index 324c0369224..3a0969059bf 100644 --- a/tests/module/Ieee80211MgmtApChannelChange_1.test +++ b/tests/module/Ieee80211MgmtApChannelChange_1.test @@ -109,9 +109,9 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp rates.numRates = 1; rates.rate[0] = 6; body->setSupportedRates(rates); - if (mib->isHtOperationSupported()) { + if (mib->isLocalHtCapable()) { body->setHtCapabilitiesPresent(true); - auto staCaps = mib->localHtCapabilities; + auto staCaps = mib->getLocalHtCapabilities(); staCaps.supportedChannelWidths.insert(MHz(20)); staCaps.supportedChannelWidths.insert(MHz(40)); body->setHtCapabilities(makeHtCapabilitiesElement(staCaps)); @@ -188,9 +188,9 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule const auto *peerState = mib->findPeerHtState(staAddress); ASSERT(peerState != nullptr); ASSERT(peerState->valid); - ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 0); - ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 1); - ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(40)); // Verify beacon advertised HT Operation matches 40 MHz SCA operation mgmt->emitBeaconNow(); @@ -204,7 +204,7 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule // Verify rate selection selects 40 MHz mode for peer const auto *mcs7_40 = findHtMode(modeSet, 7, MHz(40)); ASSERT(mcs7_40 != nullptr); - const auto *selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress); + const auto *selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress, &mib->getHtOperation(), mib->relationshipAllowsHt(staAddress)); ASSERT(selectedMode == mcs7_40); std::cout << "Station associated and negotiated 40 MHz HT operation.\n"; @@ -222,9 +222,9 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule peerState = mib->findPeerHtState(staAddress); ASSERT(peerState != nullptr); ASSERT(peerState->valid); - ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 10); - ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 0); - ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(20)); + ASSERT(mib->getHtOperation().primaryChannel == 10); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 0); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(20)); // Verify beacon advertised HT Operation reflects 20 MHz BSS operation mgmt->emitBeaconNow(); @@ -234,7 +234,7 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule ASSERT(!beaconElem.staChannelWidth40Mhz); // Verify rate selection for existing peer restricts to 20 MHz - selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress); + selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress, &mib->getHtOperation(), mib->relationshipAllowsHt(staAddress)); ASSERT(selectedMode != nullptr); ASSERT(selectedMode != mcs7_40); ASSERT(selectedMode->getDataMode()->getBandwidth() == MHz(20)); @@ -253,9 +253,9 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule peerState = mib->findPeerHtState(staAddress); ASSERT(peerState != nullptr); ASSERT(peerState->valid); - ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 0); - ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 1); - ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(40)); // Verify beacon advertised HT Operation restored to 40 MHz mgmt->emitBeaconNow(); @@ -265,7 +265,7 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule ASSERT(beaconElem.staChannelWidth40Mhz); // Verify rate selection permits 40 MHz again - selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress); + selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress, &mib->getHtOperation(), mib->relationshipAllowsHt(staAddress)); ASSERT(selectedMode == mcs7_40); std::cout << "Dynamic channel change back to 0 restored 40 MHz HT operation.\n"; @@ -283,7 +283,9 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule bool threwMibInvalidChannel = false; try { - mib->setPrimaryChannel(99, mgmt->getHtOperationBand()); + auto invalidOperation = mib->getHtOperation(); + invalidOperation.primaryChannel = 99; + mib->commitBss("test", mib->address, mgmt->getHtOperationBand(), 99, &invalidOperation); } catch (const cRuntimeError& e) { threwMibInvalidChannel = true; @@ -313,9 +315,9 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule peerState = mib->findPeerHtState(staAddress); ASSERT(peerState != nullptr); ASSERT(peerState->valid); - ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 0); - ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 1); - ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(40)); // Advertised beacon reflects new band standard channel number (36) mgmt->emitBeaconNow(); @@ -327,7 +329,7 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule // Also verify that band change to a band without standard channel numbers throws bool threwInvalidBand = false; try { - mib->setPrimaryChannel(0, &physicallayer::Ieee80211CompliantBands::band5GHz); + mib->commitBss("test", mib->address, &physicallayer::Ieee80211CompliantBands::band5GHz, 0, &mib->getHtOperation()); } catch (const cRuntimeError& e) { threwInvalidBand = true; @@ -342,8 +344,9 @@ class Ieee80211MgmtApChannelChangeTest : public cSimpleModule // 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. - mib->updateLocalHtCapabilities(nullptr, {}, 0); - ASSERT(!mib->isHtOperationSupported()); + mib->clearBss(); + mib->installLocalHtCapabilities(Ieee80211HtCapabilities(), false); + ASSERT(!mib->isLocalHtCapable()); bool threwNonHtBandChange = false; try { radio->setBand(&physicallayer::Ieee80211CompliantBands::band5GHz); diff --git a/tests/module/Ieee80211MgmtApGenericRadio_1.test b/tests/module/Ieee80211MgmtApGenericRadio_1.test index 130617f9d90..0113dbbd6b9 100644 --- a/tests/module/Ieee80211MgmtApGenericRadio_1.test +++ b/tests/module/Ieee80211MgmtApGenericRadio_1.test @@ -24,7 +24,7 @@ class TestIeee80211MgmtApGenericRadio : public Ieee80211MgmtAp { Ieee80211MgmtAp::initialize(stage); if (stage == INITSTAGE_LAST) { - ASSERT(!mib->isHtOperationSupported()); + ASSERT(!mib->isLocalHtCapable()); ASSERT(!mib->hasPrimaryChannel()); probeTimer = new cMessage("injectProbe"); scheduleAt(SimTime(2, SIMTIME_MS), probeTimer); diff --git a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test index 843fb1ade0b..3f301e472f6 100644 --- a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test @@ -46,7 +46,7 @@ class TestIeee80211MgmtApHcfQueueDrop : public Ieee80211MgmtAp Enter_Method("markAuthenticated"); auto& sta = staList[address]; sta.address = address; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); } void submitAssociationRequest(const MacAddress& address) @@ -95,10 +95,10 @@ class TestIeee80211MgmtApHcfQueueDrop : public Ieee80211MgmtAp void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } short getCommittedAssociationId(const MacAddress& address) const { - auto it = mib->bssAccessPointData.associationIds.find(address); - return it == mib->bssAccessPointData.associationIds.end() ? 0 : it->second; + auto it = mib->getBssAccessPointData().associationIds.find(address); + return it == mib->getBssAccessPointData().associationIds.end() ? 0 : it->second; } - Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->bssAccessPointData.stations.at(address); } + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->getBssAccessPointData().stations.at(address); } protected: virtual void frameTransmissionFinished(const Packet *responseFrame, FrameTransmissionStatus status) override diff --git a/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test b/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test index 81a602e75f5..2b7a597b3ab 100644 --- a/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test +++ b/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test @@ -33,7 +33,7 @@ class TestIeee80211MgmtApHcf : public Ieee80211MgmtAp { auto& sta = staList[address]; sta.address = address; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); } short submitAssociationRequest(const MacAddress& address) @@ -70,12 +70,12 @@ class TestIeee80211MgmtApHcf : public Ieee80211MgmtAp bool hasCommittedAssociationId(const MacAddress& address) const { - return mib->bssAccessPointData.associationIds.find(address) != mib->bssAccessPointData.associationIds.end(); + return mib->getBssAccessPointData().associationIds.find(address) != mib->getBssAccessPointData().associationIds.end(); } Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { - return mib->bssAccessPointData.stations.at(address); + return mib->getBssAccessPointData().stations.at(address); } protected: diff --git a/tests/module/Ieee80211MgmtApLifecycle_1.test b/tests/module/Ieee80211MgmtApLifecycle_1.test index f91973e1096..f2f726667a5 100644 --- a/tests/module/Ieee80211MgmtApLifecycle_1.test +++ b/tests/module/Ieee80211MgmtApLifecycle_1.test @@ -69,9 +69,9 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp rates.numRates = 1; rates.rate[0] = 6; body->setSupportedRates(rates); - if (mib->isHtOperationSupported()) { + if (mib->isLocalHtCapable()) { body->setHtCapabilitiesPresent(true); - body->setHtCapabilities(makeHtCapabilitiesElement(mib->localHtCapabilities)); + body->setHtCapabilities(makeHtCapabilitiesElement(mib->getLocalHtCapabilities())); } body->setChunkLength(B(100)); packet->insertAtBack(body); @@ -103,18 +103,18 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp bool hasStation(const MacAddress& address) const { - return mib->bssAccessPointData.stations.find(address) != mib->bssAccessPointData.stations.end(); + return mib->getBssAccessPointData().stations.find(address) != mib->getBssAccessPointData().stations.end(); } Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { - auto it = mib->bssAccessPointData.stations.find(address); - return it == mib->bssAccessPointData.stations.end() ? Ieee80211Mib::NOT_AUTHENTICATED : it->second; + auto it = mib->getBssAccessPointData().stations.find(address); + return it == mib->getBssAccessPointData().stations.end() ? Ieee80211Mib::NOT_AUTHENTICATED : it->second; } bool hasCommittedAid(const MacAddress& address) const { - return mib->bssAccessPointData.associationIds.find(address) != mib->bssAccessPointData.associationIds.end(); + return mib->getBssAccessPointData().associationIds.find(address) != mib->getBssAccessPointData().associationIds.end(); } bool hasPeerHtState(const MacAddress& address) const @@ -159,12 +159,18 @@ class Ieee80211MgmtApLifecycleTest : public cSimpleModule wait(SimTime(15, SIMTIME_US)); // wait until t=15us (AP is down) // Verify AP teardown: stations, AIDs, and peer HT states are completely cleared - ASSERT(mib->bssAccessPointData.stations.empty()); - ASSERT(mib->bssAccessPointData.associationIds.empty()); + ASSERT(mib->getBssAccessPointData().stations.empty()); + ASSERT(mib->getBssAccessPointData().associationIds.empty()); ASSERT(mgmt->isStaListEmpty()); ASSERT(!mgmt->hasPeerHtState(staAddress)); std::cout << "AP lifecycle teardown cleared all station, AID, and peer HT states.\n"; + ASSERT(!mib->hasActiveBss() && !mib->hasHtOperation()); + bool rejectedPreparation = false; + try { mgmt->prepareBss(); } + catch (const cRuntimeError&) { rejectedPreparation = true; } + ASSERT(rejectedPreparation && !mib->hasActiveBss()); + // 3. AP is restarted at t=20us via ScenarioManager wait(SimTime(10, SIMTIME_US)); // wait until t=25us (AP is restarted and up) diff --git a/tests/module/Ieee80211MgmtApMalformedHtCap_1.test b/tests/module/Ieee80211MgmtApMalformedHtCap_1.test index 7520d813245..ce5af20ec4a 100644 --- a/tests/module/Ieee80211MgmtApMalformedHtCap_1.test +++ b/tests/module/Ieee80211MgmtApMalformedHtCap_1.test @@ -62,7 +62,7 @@ class Ieee80211MgmtApMalformedHtCapAp : public Ieee80211MgmtAp { auto& sta = staList[address]; sta.address = address; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); } void submitAssociationRequest(const MacAddress& address, bool malformed, short exponent = 4) @@ -76,7 +76,7 @@ class Ieee80211MgmtApMalformedHtCapAp : public Ieee80211MgmtAp rates.rate[0] = 1; body->setSupportedRates(rates); body->setHtCapabilitiesPresent(true); - auto elem = makeHtCapabilitiesElement(mib->localHtCapabilities); + auto elem = makeHtCapabilitiesElement(mib->getLocalHtCapabilities()); if (malformed) elem.maxAmpduLengthExponent = exponent; body->setHtCapabilities(elem); @@ -118,7 +118,7 @@ class Ieee80211MgmtApMalformedHtCapAp : public Ieee80211MgmtAp rates.rate[0] = 1; body->setSupportedRates(rates); body->setHtCapabilitiesPresent(true); - auto elem = makeHtCapabilitiesElement(mib->localHtCapabilities); + auto elem = makeHtCapabilitiesElement(mib->getLocalHtCapabilities()); if (malformed) elem.maxAmpduLengthExponent = exponent; body->setHtCapabilities(elem); @@ -180,9 +180,9 @@ class Ieee80211MgmtApMalformedHtCapTest : public cSimpleModule ASSERT(mgmt->hasPendingAssociation(sta1)); mgmt->finishResponse(sta1, ST_ASSOCIATIONRESPONSE); ASSERT(!mgmt->hasPendingAssociation(sta1)); - ASSERT(mib->bssAccessPointData.stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); + ASSERT(mib->getBssAccessPointData().stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); ASSERT(mib->findPeerHtState(sta1) == nullptr); - ASSERT(mib->bssAccessPointData.associationIds.find(sta1) == mib->bssAccessPointData.associationIds.end()); + ASSERT(mib->getBssAccessPointData().associationIds.find(sta1) == mib->getBssAccessPointData().associationIds.end()); std::cout << "Malformed AssociationRequest (exponent 4) refused with SC_UNSUP_CAP.\n"; // 2. Malformed AssociationRequest (exponent = -1) refused with SC_UNSUP_CAP @@ -193,7 +193,7 @@ class Ieee80211MgmtApMalformedHtCapTest : public cSimpleModule ASSERT(mgmt->hasPendingAssociation(sta1)); mgmt->finishResponse(sta1, ST_ASSOCIATIONRESPONSE); ASSERT(!mgmt->hasPendingAssociation(sta1)); - ASSERT(mib->bssAccessPointData.stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); + ASSERT(mib->getBssAccessPointData().stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); ASSERT(mib->findPeerHtState(sta1) == nullptr); std::cout << "Malformed AssociationRequest (exponent -1) refused with SC_UNSUP_CAP.\n"; @@ -204,7 +204,7 @@ class Ieee80211MgmtApMalformedHtCapTest : public cSimpleModule ASSERT(mgmt->sentFrames.back().aid > 0); mgmt->finishResponse(sta1, ST_ASSOCIATIONRESPONSE); ASSERT(!mgmt->hasPendingAssociation(sta1)); - ASSERT(mib->bssAccessPointData.stations.at(sta1) == Ieee80211Mib::ASSOCIATED); + ASSERT(mib->getBssAccessPointData().stations.at(sta1) == Ieee80211Mib::ASSOCIATED); ASSERT(mib->findPeerHtState(sta1) != nullptr); std::cout << "Valid AssociationRequest succeeded with SC_SUCCESSFUL.\n"; @@ -217,9 +217,9 @@ class Ieee80211MgmtApMalformedHtCapTest : public cSimpleModule ASSERT(mgmt->hasPendingAssociation(sta1)); mgmt->finishResponse(sta1, ST_REASSOCIATIONRESPONSE); ASSERT(!mgmt->hasPendingAssociation(sta1)); - ASSERT(mib->bssAccessPointData.stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); + ASSERT(mib->getBssAccessPointData().stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); ASSERT(mib->findPeerHtState(sta1) == nullptr); - ASSERT(mib->bssAccessPointData.associationIds.find(sta1) == mib->bssAccessPointData.associationIds.end()); + ASSERT(mib->getBssAccessPointData().associationIds.find(sta1) == mib->getBssAccessPointData().associationIds.end()); std::cout << "Malformed ReassociationRequest (exponent 4) refused with SC_UNSUP_CAP and cleared association.\n"; // 5. Malformed ReassociationRequest (exponent = -1) from authenticated station @@ -231,12 +231,12 @@ class Ieee80211MgmtApMalformedHtCapTest : public cSimpleModule ASSERT(mgmt->hasPendingAssociation(sta2)); mgmt->finishResponse(sta2, ST_REASSOCIATIONRESPONSE); ASSERT(!mgmt->hasPendingAssociation(sta2)); - ASSERT(mib->bssAccessPointData.stations.at(sta2) == Ieee80211Mib::AUTHENTICATED); + ASSERT(mib->getBssAccessPointData().stations.at(sta2) == Ieee80211Mib::AUTHENTICATED); ASSERT(mib->findPeerHtState(sta2) == nullptr); std::cout << "Malformed ReassociationRequest (exponent -1) refused with SC_UNSUP_CAP.\n"; // 6. Malformed AssociationRequest with contradictory Tx MCS set fields refused with SC_UNSUP_CAP - auto badTxElem = makeHtCapabilitiesElement(mib->localHtCapabilities); + auto badTxElem = makeHtCapabilitiesElement(mib->getLocalHtCapabilities()); badTxElem.txMcsSetDefined = false; badTxElem.txRxMcsSetNotEqual = true; mgmt->submitAssociationRequestWithElement(sta1, badTxElem); @@ -246,7 +246,7 @@ class Ieee80211MgmtApMalformedHtCapTest : public cSimpleModule ASSERT(mgmt->hasPendingAssociation(sta1)); mgmt->finishResponse(sta1, ST_ASSOCIATIONRESPONSE); ASSERT(!mgmt->hasPendingAssociation(sta1)); - ASSERT(mib->bssAccessPointData.stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); + ASSERT(mib->getBssAccessPointData().stations.at(sta1) == Ieee80211Mib::AUTHENTICATED); ASSERT(mib->findPeerHtState(sta1) == nullptr); std::cout << "Malformed AssociationRequest (contradictory Tx MCS) refused with SC_UNSUP_CAP.\n"; } diff --git a/tests/module/Ieee80211MgmtApQueueDrop_1.test b/tests/module/Ieee80211MgmtApQueueDrop_1.test index 44a499578c2..7a4444bd1ad 100644 --- a/tests/module/Ieee80211MgmtApQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApQueueDrop_1.test @@ -36,7 +36,7 @@ class TestIeee80211MgmtApQueueDrop : public Ieee80211MgmtAp Enter_Method("markAuthenticated"); auto& sta = staList[address]; sta.address = address; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); } void submitAssociationRequest(const MacAddress& address) @@ -66,7 +66,7 @@ class TestIeee80211MgmtApQueueDrop : public Ieee80211MgmtAp short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } - Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->bssAccessPointData.stations.at(address); } + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->getBssAccessPointData().stations.at(address); } protected: virtual void frameTransmissionFinished(const Packet *frame, FrameTransmissionStatus status) override diff --git a/tests/module/Ieee80211MgmtApReassociationSnapshot_1.test b/tests/module/Ieee80211MgmtApReassociationSnapshot_1.test index fa3e2efcbd0..6499d04c5d2 100644 --- a/tests/module/Ieee80211MgmtApReassociationSnapshot_1.test +++ b/tests/module/Ieee80211MgmtApReassociationSnapshot_1.test @@ -41,7 +41,7 @@ class Ieee80211MgmtApReassociationSnapshotAp : public Ieee80211MgmtAp { auto& sta = staList[address]; sta.address = address; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); } void submitReassociationRequest(const MacAddress& address) @@ -56,7 +56,7 @@ class Ieee80211MgmtApReassociationSnapshotAp : public Ieee80211MgmtAp rates.rate[0] = 1; body->setSupportedRates(rates); body->setHtCapabilitiesPresent(true); - body->setHtCapabilities(makeHtCapabilitiesElement(mib->localHtCapabilities)); + body->setHtCapabilities(makeHtCapabilitiesElement(mib->getLocalHtCapabilities())); body->setChunkLength(B(1)); packet->insertAtBack(body); auto header = makeShared(); @@ -93,6 +93,30 @@ class Ieee80211MgmtApReassociationSnapshotAp : public Ieee80211MgmtAp Define_Module(Ieee80211MgmtApReassociationSnapshotAp); +class ApCommitObserver : public cListener +{ + public: + Ieee80211MgmtApReassociationSnapshotAp *management; + MacAddress peer; + int notifications = 0; + bool expectPending = true; + ApCommitObserver(Ieee80211MgmtApReassociationSnapshotAp *management, const MacAddress& peer) : management(management), peer(peer) {} + virtual void receiveSignal(cComponent *source, simsignal_t, bool active, cObject *) override + { + auto *mib = check_and_cast(source); + notifications++; + ASSERT(active && mib->hasHtOperation()); + ASSERT(mib->getOperationBand() != nullptr); + ASSERT(mib->requirePrimaryChannel() == mib->getHtOperation().primaryChannel); + ASSERT(management->hasPendingAssociation(peer) == expectPending); + if (!expectPending) { + ASSERT(mib->getPeerAssociationStatus(peer) == Ieee80211Mib::ASSOCIATED); + ASSERT(mib->getBssAccessPointData().associationIds.at(peer) > 0); + ASSERT(mib->findPeerHtState(peer) != nullptr); + } + } +}; + class Ieee80211MgmtApReassociationSnapshotTest : public cSimpleModule { public: @@ -111,15 +135,28 @@ class Ieee80211MgmtApReassociationSnapshotTest : public cSimpleModule ASSERT(mgmt->getAdvertisedPrimaryChannel() == 7); ASSERT(mib->requirePrimaryChannel() == 6); + ApCommitObserver observer(mgmt, staAddress); + mib->subscribe(Ieee80211Mib::bssStateChangedSignal, &observer); radio->setChannelNumber(11); ASSERT(mib->requirePrimaryChannel() == 11); + observer.expectPending = false; mgmt->finishReassociationResponse(staAddress); ASSERT(!mgmt->hasPendingAssociation(staAddress)); - ASSERT(mib->bssAccessPointData.stations.at(staAddress) == Ieee80211Mib::ASSOCIATED); + ASSERT(mib->getBssAccessPointData().stations.at(staAddress) == Ieee80211Mib::ASSOCIATED); const auto *peerState = mib->findPeerHtState(staAddress); ASSERT(peerState != nullptr); - ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 11); + ASSERT(mib->getHtOperation().primaryChannel == 11); + auto previous = peerState->negotiatedCapabilities; + auto previousAid = mib->getBssAccessPointData().associationIds.at(staAddress); + mgmt->submitReassociationRequest(staAddress); + ASSERT(mib->findPeerHtState(staAddress)->negotiatedCapabilities == previous); + mgmt->finishReassociationResponse(staAddress); + ASSERT(mib->findPeerHtState(staAddress)->negotiatedCapabilities != previous); + ASSERT(mib->getBssAccessPointData().associationIds.at(staAddress) == previousAid); + ASSERT(observer.notifications == 3); + mib->unsubscribe(Ieee80211Mib::bssStateChangedSignal, &observer); + std::cout << "AP observers see completed transactions; equal same-address replacement creates a fresh capability cache.\n"; std::cout << "AP reassociation reconciles committed HT Operation with current channel after channel change.\n"; } }; @@ -188,3 +225,6 @@ record-scalar-results = false %contains: stdout AP reassociation reconciles committed HT Operation with current channel after channel change. + +%contains: stdout +AP observers see completed transactions; equal same-address replacement creates a fresh capability cache. diff --git a/tests/module/Ieee80211MgmtApTimeout_1.test b/tests/module/Ieee80211MgmtApTimeout_1.test index 54b9bfe4916..527fc8e7ce5 100644 --- a/tests/module/Ieee80211MgmtApTimeout_1.test +++ b/tests/module/Ieee80211MgmtApTimeout_1.test @@ -91,7 +91,7 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp auto& sta = staList[address]; sta.address = address; clearPendingAssociation(&sta); - mib->bssAccessPointData.stations[address] = status; + mib->setPeerAssociationStatus(address, status); short aid = mib->reserveAssociationId(address); sta.pendingAssociationSuccessful = true; sta.pendingAssociationTransactionId = createAssociationTransactionId(); @@ -105,9 +105,9 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp sta.address = address; clearPendingAssociation(&sta); short aid = mib->allocateAssociationId(address); - mib->bssAccessPointData.stations[address] = Ieee80211Mib::ASSOCIATED; - if (mib->isHtOperationSupported()) - mib->setPeerHtCapabilities(address, mib->localHtCapabilities, mib->getHtOperation()); + mib->setPeerAssociationStatus(address, Ieee80211Mib::ASSOCIATED); + if (mib->isLocalHtCapable()) + mib->setPeerHtCapabilities(address, mib->getLocalHtCapabilities()); ASSERT(mib->reserveAssociationId(address) == aid); sta.pendingAssociationSuccessful = true; sta.pendingAssociationTransactionId = createAssociationTransactionId(); @@ -158,15 +158,15 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } - bool hasCommittedAssociationId(const MacAddress& address) const { return mib->bssAccessPointData.associationIds.find(address) != mib->bssAccessPointData.associationIds.end(); } + bool hasCommittedAssociationId(const MacAddress& address) const { return mib->getBssAccessPointData().associationIds.find(address) != mib->getBssAccessPointData().associationIds.end(); } bool hasPeerHtState(const MacAddress& address) const { return mib->findPeerHtState(address) != nullptr; } - short getCommittedAssociationId(const MacAddress& address) const { return mib->bssAccessPointData.associationIds.at(address); } - Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->bssAccessPointData.stations.at(address); } + short getCommittedAssociationId(const MacAddress& address) const { return mib->getBssAccessPointData().associationIds.at(address); } + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->getBssAccessPointData().stations.at(address); } void markAuthenticated(const MacAddress& address) { auto& sta = staList[address]; sta.address = address; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); } void setAuthenticationSequenceExpected(const MacAddress& address, int sequenceNumber) diff --git a/tests/module/Ieee80211MgmtStaBeaconUpdate_1.test b/tests/module/Ieee80211MgmtStaBeaconUpdate_1.test index af38f0eb4a8..2c48332ac68 100644 --- a/tests/module/Ieee80211MgmtStaBeaconUpdate_1.test +++ b/tests/module/Ieee80211MgmtStaBeaconUpdate_1.test @@ -47,18 +47,22 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta associationConfirms++; } + Ieee80211HtOperation testOperation; + void setPrimaryChannelForTest(int channel) { Enter_Method("setPrimaryChannelForTest"); - mib->setPrimaryChannel(channel); + testOperation.primaryChannel = channel; } void enable40MhzLocalCapabilities() { Enter_Method("enable40MhzLocalCapabilities"); - mib->localHtCapabilities.supportedChannelWidths.insert(MHz(40)); - mib->localHtCapabilities.shortGi20 = true; - mib->localHtCapabilities.shortGi40 = true; + auto capabilities = mib->getLocalHtCapabilities(); + capabilities.supportedChannelWidths.insert(MHz(40)); + capabilities.shortGi20 = true; + capabilities.shortGi40 = true; + mib->installLocalHtCapabilities(capabilities, true); } const ApInfo *getCachedAp(const MacAddress& address) const @@ -66,7 +70,7 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta return const_cast(this)->lookupAP(address); } - bool isAssociatedForTest() const { return mib->bssStationData.isAssociated; } + bool isAssociatedForTest() const { return mib->getBssStationData().isAssociated; } const AssociatedApInfo& getAssocApForTest() const { return assocAP; } bool hasPeerHtState(const MacAddress& address) const { return mib->findPeerHtState(address) != nullptr; } const Ieee80211Mib::PeerHtState *getPeerHtState(const MacAddress& address) const { return mib->findPeerHtState(address); } @@ -82,7 +86,7 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta apList.push_back(ApInfo()); ap = &apList.back(); ap->address = address; - ap->channel = mib->getHtOperation().primaryChannel; + ap->channel = testOperation.primaryChannel; ap->ssid = "test-bss"; ap->beaconInterval = SimTime(100, SIMTIME_MS); } @@ -109,8 +113,8 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta rates.rate[0] = 6; body->setSupportedRates(rates); - auto capabilities = mib->localHtCapabilities; - auto operation = mib->getHtOperation(); + Ieee80211HtCapabilities capabilities; + auto operation = testOperation; operation.operatingChannelWidth = MHz(responseOperatingChannelWidth); operation.secondaryChannelOffset = responseSecondaryChannelOffset; capabilities.supportedChannelWidths.insert(MHz(20)); @@ -130,7 +134,7 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta body->setChunkLength(B(9) + getHtMgmtElementsLength(body)); auto packet = new Packet("AssociationResponse"); packet->insertAtBack(body); - int channelNumber = mib->getHtOperation().primaryChannel; + int channelNumber = testOperation.primaryChannel; physicallayer::Ieee80211Channel channel(band, channelNumber); packet->addTag()->setChannel(&channel); auto header = makeShared(); @@ -152,7 +156,7 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta Enter_Method("deliverBeaconFrame"); auto body = makeShared(); body->setSSID("test-bss"); - int primaryChannel = mib->getHtOperation().primaryChannel; + int primaryChannel = testOperation.primaryChannel; body->setChannelNumber(primaryChannel); body->setBeaconInterval(beaconInterval); Ieee80211SupportedRatesElement rates; @@ -163,8 +167,8 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta if (htPresent) { Ieee80211HtCapabilities capabilities; capabilities.supportedChannelWidths.insert(MHz(20)); - if (operatingChannelWidth == 40) - capabilities.supportedChannelWidths.insert(MHz(40)); + // Capability stays 20/40 MHz when only BSS operation narrows. + capabilities.supportedChannelWidths.insert(MHz(40)); capabilities.shortGi20 = shortGi20; capabilities.shortGi40 = shortGi40; int maxMcs = -1; @@ -218,7 +222,7 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta Enter_Method("deliverProbeResponseFrame"); auto body = makeShared(); body->setSSID("test-bss"); - int primaryChannel = mib->getHtOperation().primaryChannel; + int primaryChannel = testOperation.primaryChannel; body->setChannelNumber(primaryChannel); body->setBeaconInterval(SimTime(100, SIMTIME_MS)); Ieee80211SupportedRatesElement rates; @@ -263,6 +267,38 @@ class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta Define_Module(TestIeee80211MgmtStaBeaconUpdate); +// Synchronous observer checks the production commit boundary, including the +// owning management module's timer and accepted snapshot, without retaining borrows. +class BssCommitObserver : public cListener +{ + public: + TestIeee80211MgmtStaBeaconUpdate *management; + int notifications = 0; + explicit BssCommitObserver(TestIeee80211MgmtStaBeaconUpdate *management) : management(management) {} + virtual void receiveSignal(cComponent *source, simsignal_t, bool active, cObject *) override + { + auto *mib = check_and_cast(source); + notifications++; + ASSERT(active && mib->hasActiveBss()); + ASSERT(mib->getBssStationData().isAssociated); + const auto& accepted = management->getAssocApForTest(); + ASSERT(mib->getBssData().bssid == accepted.address); + ASSERT(mib->getBssData().ssid == accepted.ssid); + ASSERT(mib->getOperationBand() != nullptr); + ASSERT(mib->requirePrimaryChannel() == accepted.channel); + ASSERT(mib->hasHtOperation() == accepted.htOperationPresent); + if (mib->hasHtOperation()) + ASSERT(mib->getHtOperation() == accepted.htOperation); + ASSERT(management->hasBeaconTimeoutForTest()); + ASSERT(management->getBeaconTimeoutArrivalForTest() == simTime() + 3.5 * accepted.beaconInterval); + // The documented notification contract rejects nested state mutations. + bool rejected = false; + try { mib->clearBss(); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected && mib->hasActiveBss()); + } +}; + class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule { public: @@ -272,6 +308,7 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule virtual void activity() override { auto mgmt = check_and_cast(getModuleByPath("^.sta.wlan[0].mgmt")); + auto staMib = check_and_cast(getModuleByPath("^.sta.wlan[0].mib")); auto dcfRateSelection = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateSelection")); auto qosRateSelection = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.hcf.rateSelection")); const auto *modeSet = physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); @@ -296,6 +333,9 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule } }; + BssCommitObserver first(mgmt), second(mgmt); + staMib->subscribe(Ieee80211Mib::bssStateChangedSignal, &first); + staMib->subscribe(Ieee80211Mib::bssStateChangedSignal, &second); mgmt->setPrimaryChannelForTest(6); mgmt->enable40MhzLocalCapabilities(); @@ -318,6 +358,8 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule // Verify rate selection selects 40 MHz Short GI MCS 7 (150 Mbps) checkUnicastMode(MHz(40), true, 7); + auto initialCache = mgmt->getPeerHtState(apAddress)->negotiatedCapabilities; + // 3. Subsequent Beacon from associated AP switches HT Operation to 20 MHz wait(SimTime(1, SIMTIME_US)); mgmt->deliverBeaconFrame(apAddress, true, 20, 0, true, true); @@ -325,17 +367,33 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule ASSERT(mgmt->hasPeerHtState(apAddress)); ASSERT(mgmt->getAssocApForTest().htOperation.operatingChannelWidth == MHz(20)); ASSERT(mgmt->getAssocApForTest().htOperation.secondaryChannelOffset == 0); - ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.operation.operatingChannelWidth == MHz(20)); + ASSERT(staMib->getHtOperation().operatingChannelWidth == MHz(20)); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities == initialCache); // Rate selection must demote to 20 MHz Short GI MCS 7; cannot retain obsolete 40 MHz constraint checkUnicastMode(MHz(20), true, 7); + ASSERT(first.notifications == 2 && second.notifications == 2); + // Equal accepted information refreshes liveness, without rederivation or publication. + auto deadlineBeforeEqual = mgmt->getBeaconTimeoutArrivalForTest(); + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverBeaconFrame(apAddress, true, 20, 0, true, true); + ASSERT(mgmt->getBeaconTimeoutArrivalForTest() > deadlineBeforeEqual); + ASSERT(first.notifications == 2 && second.notifications == 2); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities == initialCache); + // Reverse registration order for the remaining changed and rejected inputs. + staMib->unsubscribe(Ieee80211Mib::bssStateChangedSignal, &first); + staMib->unsubscribe(Ieee80211Mib::bssStateChangedSignal, &second); + staMib->subscribe(Ieee80211Mib::bssStateChangedSignal, &second); + staMib->subscribe(Ieee80211Mib::bssStateChangedSignal, &first); + // 4. Subsequent Beacon disables Short Guard Interval (20 MHz, Long GI) wait(SimTime(1, SIMTIME_US)); mgmt->deliverBeaconFrame(apAddress, true, 20, 0, false, false); ASSERT(mgmt->isAssociatedForTest()); ASSERT(mgmt->hasPeerHtState(apAddress)); ASSERT(!mgmt->getAssocApForTest().htCapabilities.shortGi20); - ASSERT(!mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.localTxPeerRx.receiverShortGi20); + ASSERT(!mgmt->getPeerHtState(apAddress)->negotiatedCapabilities->localTxPeerRx.receiverShortGi20); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities != initialCache); // Rate selection must demote to 20 MHz Long GI MCS 7; cannot retain obsolete Short GI constraint checkUnicastMode(MHz(20), false, 7); @@ -346,8 +404,9 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule ASSERT(mgmt->hasPeerHtState(apAddress)); ASSERT(mgmt->getAssocApForTest().htCapabilities.rxMcsSupported[0]); ASSERT(!mgmt->getAssocApForTest().htCapabilities.rxMcsSupported[7]); - ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.localTxPeerRx.supportedMcs[0]); - ASSERT(!mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.localTxPeerRx.supportedMcs[7]); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities->localTxPeerRx.supportedMcs[0]); + ASSERT(!mgmt->getPeerHtState(apAddress)->negotiatedCapabilities->localTxPeerRx.supportedMcs[7]); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities != initialCache); // Rate selection must demote to MCS 0; cannot retain obsolete MCS 7 constraint checkUnicastMode(MHz(20), false, 0); @@ -372,11 +431,20 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule ASSERT(mgmt->hasBeaconTimeoutForTest()); ASSERT(mgmt->getAssocApForTest().htOperationPresent); ASSERT(mgmt->getAssocApForTest().htOperation.basicMcsSupported[32]); - // MIB peer HT state remains absent because Basic MCS is unsupported + // Accepted capabilities remain cached, but HT is ineligible because Basic MCS is unsupported ASSERT(!mgmt->hasPeerHtState(apAddress)); // Rate selection remains on legacy mode checkUnicastMode(MHz(20), false, -1); + // A Basic-MCS-only recovery retains accepted capability knowledge. + auto temporarilyIneligibleCache = staMib->findPeerCapabilities(apAddress)->negotiatedCapabilities; + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverBeaconFrame(apAddress, true, 20, 0, true, false, + {0, 1, 2, 3, 4, 5, 6, 7}, {0, 1, 2, 3, 4, 5, 6, 7}); + ASSERT(mgmt->hasPeerHtState(apAddress)); + ASSERT(staMib->findPeerCapabilities(apAddress)->negotiatedCapabilities == temporarilyIneligibleCache); + checkUnicastMode(MHz(20), true, 7); + // 8. Subsequent Beacon restores full 40 MHz Short GI HT advertisement + updates beacon interval wait(SimTime(1, SIMTIME_US)); simtime_t newBeaconInterval = SimTime(200, SIMTIME_MS); @@ -393,6 +461,8 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule // Rate selection restores 40 MHz Short GI MCS 7 (150 Mbps) checkUnicastMode(MHz(40), true, 7); + int notificationsBeforeRejected = first.notifications; + // 9. Subsequent malformed Beacon (invalid secondaryChannelOffset = 2) is rejected wait(SimTime(1, SIMTIME_US)); simtime_t deadlineBeforeMalformed = mgmt->getBeaconTimeoutArrivalForTest(); @@ -411,9 +481,14 @@ class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule // Cached AP list has the probe response, but assocAP and MIB peer HT state retain 40 MHz SGI ASSERT(mgmt->getCachedAp(apAddress)->htOperation.operatingChannelWidth == MHz(20)); ASSERT(mgmt->getAssocApForTest().htOperation.operatingChannelWidth == MHz(40)); - ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + ASSERT(staMib->getHtOperation().operatingChannelWidth == MHz(40)); checkUnicastMode(MHz(40), true, 7); + ASSERT(first.notifications == notificationsBeforeRejected); + ASSERT(first.notifications == second.notifications); + staMib->unsubscribe(Ieee80211Mib::bssStateChangedSignal, &first); + staMib->unsubscribe(Ieee80211Mib::bssStateChangedSignal, &second); + std::cout << "Atomic BSS observers, equal-beacon liveness, and reentrancy contract verified.\n"; std::cout << "Subsequent Beacon updates authoritative associated-AP snapshot and MIB peer HT state.\n"; std::cout << "Unicast rate selection follows dynamic channel-width, guard-interval, and MCS updates.\n"; std::cout << "Legacy and unusable HT updates preserve association while falling back to legacy modes.\n"; @@ -502,3 +577,6 @@ Unicast rate selection follows dynamic channel-width, guard-interval, and MCS up %contains: stdout Legacy and unusable HT updates preserve association while falling back to legacy modes. + +%contains: stdout +Atomic BSS observers, equal-beacon liveness, and reentrancy contract verified. diff --git a/tests/module/Ieee80211MgmtStaDeauthentication_1.test b/tests/module/Ieee80211MgmtStaDeauthentication_1.test index 956c1db7728..84edea91127 100644 --- a/tests/module/Ieee80211MgmtStaDeauthentication_1.test +++ b/tests/module/Ieee80211MgmtStaDeauthentication_1.test @@ -42,11 +42,11 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta void setAssociated(const MacAddress& address, bool cacheAp = true) { Enter_Method("setAssociated"); - ASSERT(!mib->bssStationData.isAssociated); + ASSERT(!mib->getBssStationData().isAssociated); if (cacheAp) ensureAccessPoint(address); - mib->bssData.bssid = address; - mib->bssStationData.isAssociated = true; + mib->commitBss("SSID", address, nullptr, -1, nullptr); + mib->setAssociated(true); assocAP = AssociatedApInfo(); assocAP.address = address; assocAP.channel = 1; @@ -60,8 +60,10 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta void installPeerHtState(const MacAddress& address) { Enter_Method("installPeerHtState"); - mib->setPrimaryChannel(1); - mib->setPeerHtCapabilities(address, mib->localHtCapabilities, mib->getHtOperation()); + Ieee80211HtOperation operation; + operation.primaryChannel = 1; + mib->commitBss("SSID", address, nullptr, 1, &operation); + mib->setPeerHtCapabilities(address, mib->getLocalHtCapabilities()); } void authenticatePeer(const MacAddress& address) @@ -90,7 +92,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta bool hasPendingAssociation() const { return assocTimeoutMsg != nullptr; } bool isPendingAssociationScheduled() const { return assocTimeoutMsg != nullptr && assocTimeoutMsg->isScheduled(); } bool isReassociationPending() const { return reassociationInProgress; } - bool isAssociated() const { return mib->bssStationData.isAssociated; } + bool isAssociated() const { return mib->getBssStationData().isAssociated; } bool hasBeaconTimer() const { return assocAP.beaconTimeoutMsg != nullptr; } MacAddress getAssociatedAddress() const { return assocAP.address; } bool isAuthenticated(const MacAddress& address) const @@ -131,9 +133,9 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta lastAssociationResult = resultCode; lastAssociationConfirmationSawTerminalState = !ap->isAuthenticated && assocTimeoutMsg == nullptr; confirmationSawClearedAssociation = confirmationSawClearedAssociation || - (!mib->bssStationData.isAssociated && assocAP.address.isUnspecified()); + (!mib->getBssStationData().isAssociated && assocAP.address.isUnspecified()); confirmationSawDeauthenticatedAssociation = confirmationSawDeauthenticatedAssociation || - (!mib->bssStationData.isAssociated && assocAP.address.isUnspecified() && !ap->isAuthenticated); + (!mib->getBssStationData().isAssociated && assocAP.address.isUnspecified() && !ap->isAuthenticated); } virtual void sendReassociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode) override @@ -143,9 +145,9 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta lastReassociationResult = resultCode; lastReassociationConfirmationSawTerminalState = !ap->isAuthenticated && assocTimeoutMsg == nullptr; confirmationSawClearedAssociation = confirmationSawClearedAssociation || - (!mib->bssStationData.isAssociated && assocAP.address.isUnspecified()); + (!mib->getBssStationData().isAssociated && assocAP.address.isUnspecified()); confirmationSawDeauthenticatedAssociation = confirmationSawDeauthenticatedAssociation || - (!mib->bssStationData.isAssociated && assocAP.address.isUnspecified() && !ap->isAuthenticated); + (!mib->getBssStationData().isAssociated && assocAP.address.isUnspecified() && !ap->isAuthenticated); } }; diff --git a/tests/module/Ieee80211MgmtStaDisassociation_1.test b/tests/module/Ieee80211MgmtStaDisassociation_1.test index 87b77c7265b..90b660c82ee 100644 --- a/tests/module/Ieee80211MgmtStaDisassociation_1.test +++ b/tests/module/Ieee80211MgmtStaDisassociation_1.test @@ -43,10 +43,10 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta void setAssociated(const MacAddress& address) { Enter_Method("setAssociated"); - ASSERT(!mib->bssStationData.isAssociated); + ASSERT(!mib->getBssStationData().isAssociated); ensureAccessPoint(address); - mib->bssData.bssid = address; - mib->bssStationData.isAssociated = true; + mib->commitBss("SSID", address, nullptr, -1, nullptr); + mib->setAssociated(true); assocAP = AssociatedApInfo(); assocAP.address = address; assocAP.channel = 1; @@ -85,7 +85,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta bool hasPendingAssociation() const { return assocTimeoutMsg != nullptr; } bool isPendingAssociationScheduled() const { return assocTimeoutMsg != nullptr && assocTimeoutMsg->isScheduled(); } bool isReassociationPending() const { return reassociationInProgress; } - bool isAssociated() const { return mib->bssStationData.isAssociated; } + bool isAssociated() const { return mib->getBssStationData().isAssociated; } MacAddress getAssociatedAddress() const { return assocAP.address; } void deliverDisassociation(const MacAddress& transmitter, const MacAddress& address3) @@ -127,7 +127,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta lastAssociationAddress = ap->address; lastAssociationResult = resultCode; confirmationSawClearedAssociation = confirmationSawClearedAssociation || - (!mib->bssStationData.isAssociated && assocAP.address.isUnspecified()); + (!mib->getBssStationData().isAssociated && assocAP.address.isUnspecified()); } virtual void sendReassociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode) override @@ -136,7 +136,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta lastReassociationAddress = ap->address; lastReassociationResult = resultCode; confirmationSawClearedAssociation = confirmationSawClearedAssociation || - (!mib->bssStationData.isAssociated && assocAP.address.isUnspecified()); + (!mib->getBssStationData().isAssociated && assocAP.address.isUnspecified()); } }; diff --git a/tests/module/Ieee80211MgmtStaDiscovery_1.test b/tests/module/Ieee80211MgmtStaDiscovery_1.test index 530a1d23dbf..fc4650abf71 100644 --- a/tests/module/Ieee80211MgmtStaDiscovery_1.test +++ b/tests/module/Ieee80211MgmtStaDiscovery_1.test @@ -105,8 +105,8 @@ class TestIeee80211MgmtStaDiscovery : public Ieee80211MgmtSta rates.numRates = 1; rates.rate[0] = 6; body->setSupportedRates(rates); - setHtCapabilities(body, mib->localHtCapabilities); - setHtOperation(body, &physicallayer::Ieee80211CompliantBands::band2_4GHz, mib->getHtOperation()); + setHtCapabilities(body, mib->getLocalHtCapabilities()); + setHtOperation(body, &physicallayer::Ieee80211CompliantBands::band2_4GHz, getTestOperation()); if (malformedCapabilities) { auto capabilities = body->getHtCapabilities(); capabilities.maxAmpduLengthExponent = 4; @@ -122,7 +122,7 @@ class TestIeee80211MgmtStaDiscovery : public Ieee80211MgmtSta packet->insertAtBack(body); packet->addTag()->setPower(mW(1)); const auto *band = &physicallayer::Ieee80211CompliantBands::band2_4GHz; - physicallayer::Ieee80211Channel channel(band, mib->getHtOperation().primaryChannel); + physicallayer::Ieee80211Channel channel(band, getTestOperation().primaryChannel); if (channelIndicationPresent) packet->addTag()->setChannel(&channel); auto header = makeShared(); @@ -242,8 +242,8 @@ class TestIeee80211MgmtStaDiscovery : public Ieee80211MgmtSta rates.rate[0] = 6; body->setSupportedRates(rates); if (htForm != ResponseHtForm::LEGACY) { - auto capabilities = mib->localHtCapabilities; - auto operation = mib->getHtOperation(); + auto capabilities = mib->getLocalHtCapabilities(); + auto operation = getTestOperation(); if (responsePrimaryChannel >= 0) operation.primaryChannel = responsePrimaryChannel; if (responseOperatingChannelWidth == 20) @@ -281,7 +281,7 @@ class TestIeee80211MgmtStaDiscovery : public Ieee80211MgmtSta auto packet = new Packet("AssociationResponse"); packet->insertAtBack(body); const auto *band = &physicallayer::Ieee80211CompliantBands::band2_4GHz; - int channelNumber = responsePrimaryChannel >= 0 ? responsePrimaryChannel : mib->getHtOperation().primaryChannel; + int channelNumber = responsePrimaryChannel >= 0 ? responsePrimaryChannel : getTestOperation().primaryChannel; physicallayer::Ieee80211Channel channel(band, channelNumber); if (channelIndicationPresent) packet->addTag()->setChannel(&channel); @@ -294,13 +294,13 @@ class TestIeee80211MgmtStaDiscovery : public Ieee80211MgmtSta { Enter_Method("resetAssociationState"); cancelPendingAssociation(); - if (mib->bssStationData.isAssociated) + if (mib->getBssStationData().isAssociated) clearCurrentAssociation(); } bool hasPeerHtState(const MacAddress& address) const { return mib->findPeerHtState(address) != nullptr; } - bool isAssociatedForTest() const { return mib->bssStationData.isAssociated; } + bool isAssociatedForTest() const { return mib->getBssStationData().isAssociated; } const MacAddress& getAssociatedAddressForTest() const { return assocAP.address; } bool hasBeaconTimeoutForTest() const { return assocAP.beaconTimeoutMsg != nullptr; } simtime_t getBeaconTimeoutArrivalForTest() const { return assocAP.beaconTimeoutMsg->getArrivalTime(); } @@ -312,13 +312,17 @@ class TestIeee80211MgmtStaDiscovery : public Ieee80211MgmtSta void setLegacyForTest() { Enter_Method("setLegacyForTest"); - mib->localHtCapabilitiesValid = false; + mib->clearBss(); + mib->installLocalHtCapabilities(Ieee80211HtCapabilities(), false); } + Ieee80211HtOperation testOperation; + const Ieee80211HtOperation& getTestOperation() const { return testOperation; } + void setPrimaryChannelForTest(int channel) { Enter_Method("setPrimaryChannelForTest"); - mib->setPrimaryChannel(channel); + testOperation.primaryChannel = channel; } protected: @@ -445,7 +449,7 @@ class Ieee80211MgmtStaDiscoveryTest : public cSimpleModule, public cListener // reassociation to another AP keeps the current association and // restores the current AP's channel before reporting failure. mgmt->finishAuthenticationForTest(probeAddress); - staMib->setPeerHtCapabilities(probeAddress, staMib->localHtCapabilities, staMib->getHtOperation()); + staMib->setPeerHtCapabilities(probeAddress, staMib->getLocalHtCapabilities()); mgmt->clearChangedChannelsForTest(); mgmt->startReassociationForTest(probeAddress); mgmt->deliverResponse(probeAddress, true, SC_DATARATE_UNSUP, TestIeee80211MgmtStaDiscovery::ResponseHtForm::LEGACY); @@ -462,7 +466,7 @@ class Ieee80211MgmtStaDiscoveryTest : public cSimpleModule, public cListener ASSERT(mgmt->reassociationConfirms == 1); ASSERT(mgmt->lastReassociationResult == PRC_REFUSED); - staMib->setPeerHtCapabilities(probeAddress, staMib->localHtCapabilities, staMib->getHtOperation()); + staMib->setPeerHtCapabilities(probeAddress, staMib->getLocalHtCapabilities()); mgmt->clearChangedChannelsForTest(); mgmt->startReassociationForTest(probeAddress); mgmt->deliverReassociationTimeoutForTest(); @@ -500,7 +504,7 @@ class Ieee80211MgmtStaDiscoveryTest : public cSimpleModule, public cListener Ieee80211HtOperation sparseOperation; sparseOperation.operatingChannelWidth = MHz(20); sparseOperation.basicMcsSupported[0] = true; - staMib->setPeerHtCapabilities(address, sparsePeer, sparseOperation); + staMib->setPeerHtCapabilities(address, sparsePeer); const auto *dcfSparseMode = dcfRateSelection->computeMode(&ratePacket, dataHeader); const auto *qosSparseMode = qosRateSelection->computeMode(&ratePacket, dataHeader, nullptr); ASSERT(dcfSparseMode == qosSparseMode); @@ -541,7 +545,7 @@ class Ieee80211MgmtStaDiscoveryTest : public cSimpleModule, public cListener const auto *qosMulticastMode = qosRateSelection->computeMode(&ratePacket, multicastHeader, nullptr); ASSERT(dcfMulticastMode->getHtMcsIndex() >= 0); ASSERT(qosMulticastMode->getHtMcsIndex() >= 0); - staMib->setPeerHtCapabilities(address, staMib->localHtCapabilities, staMib->getHtOperation()); + staMib->setPeerHtCapabilities(address, staMib->getLocalHtCapabilities()); ASSERT(dcfRateSelection->computeMode(&ratePacket, dataHeader)->getHtMcsIndex() >= 0); ASSERT(qosRateSelection->computeMode(&ratePacket, dataHeader, nullptr)->getHtMcsIndex() >= 0); diff --git a/tests/module/Ieee80211MgmtStaLifecycle_1.test b/tests/module/Ieee80211MgmtStaLifecycle_1.test index 8873f0901e5..8d74d4b9d7b 100644 --- a/tests/module/Ieee80211MgmtStaLifecycle_1.test +++ b/tests/module/Ieee80211MgmtStaLifecycle_1.test @@ -46,7 +46,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta ASSERT(apList.empty()); ASSERT(assocTimeoutMsg == nullptr); ASSERT(!reassociationInProgress); - ASSERT(!mib->bssStationData.isAssociated); + ASSERT(!mib->getBssStationData().isAssociated); ASSERT(assocAP.address.isUnspecified()); ASSERT(assocAP.beaconTimeoutMsg == nullptr); } @@ -86,7 +86,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta { Enter_Method("startReassociationPhase"); assertCleanEntryState(); - ASSERT(mib->isHtOperationSupported()); + ASSERT(mib->isLocalHtCapable()); const auto currentAddress = getCurrentAccessPointAddress(); auto targetAp = addAccessPoint(getTargetAccessPointAddress(), true); @@ -94,9 +94,7 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta // This is the coherent state produced by a successful Association // Response: an associated AP, negotiated peer HT state, and beacon // timeout. startReassociation() then enters the real pending path. - mib->bssData.ssid = "lifecycle-ssid"; - mib->bssData.bssid = currentAddress; - mib->bssStationData.isAssociated = true; + mib->setAssociated(true); assocAP = AssociatedApInfo(); assocAP.channel = 6; assocAP.address = currentAddress; @@ -109,7 +107,8 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta Ieee80211HtOperation operation; operation.primaryChannel = 6; operation.basicMcsSupported[0] = true; - mib->setPeerHtCapabilities(currentAddress, mib->localHtCapabilities, operation); + mib->commitBss("lifecycle-ssid", currentAddress, nullptr, 6, &operation); + mib->setPeerHtCapabilities(currentAddress, mib->getLocalHtCapabilities()); startReassociation(targetAp, timerDelay); } @@ -125,13 +124,13 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta bool hasPendingAssociation() const { return assocTimeoutMsg != nullptr; } bool hasBeaconTimer() const { return assocAP.beaconTimeoutMsg != nullptr; } bool hasCurrentPeerHtState() const { return mib->findPeerHtState(getCurrentAccessPointAddress()) != nullptr; } - bool isAssociated() const { return mib->bssStationData.isAssociated; } + bool isAssociated() const { return mib->getBssStationData().isAssociated; } bool isReassociationPending() const { return reassociationInProgress; } bool isScanningNow() const { return isScanning; } bool isApListEmpty() const { return apList.empty(); } MacAddress getAssociatedAddress() const { return assocAP.address; } - const std::string& getCachedSsid() const { return mib->bssData.ssid; } - MacAddress getCachedBssid() const { return mib->bssData.bssid; } + const std::string& getCachedSsid() const { return mib->getBssData().ssid; } + MacAddress getCachedBssid() const { return mib->getBssData().bssid; } }; Define_Module(TestIeee80211MgmtSta); diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test index acc48310936..695c1203b7c 100644 --- a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -1,13 +1,16 @@ %description: Verify simplified station BSS identity is visible to automatic network -configuration while negotiated HT peer state is installed only at the last +configuration together with negotiated HT peer state, before the last initialization stage. Verify shutdown removes the AP-side association and HT peer state and startup restores both. Verify crash performs the same cleanup, -and remains safe when the recorded AP can no longer be resolved. +and remains safe when the recorded AP can no longer be resolved. Legacy stations +associated with HT APs retain BSS identity/channel without accepting HT operation, +in both declaration orders and across shutdown/crash/restart. %file: TestInitializationObserver.cc #include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mgmt/contract/IIeee80211BssProvider.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/networklayer/common/L3AddressResolver.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -28,9 +31,38 @@ class TestInitializationObserver : public cSimpleModule protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } + void checkLegacyStations(bool associated) + { + for (const char *name : {"^.legacyFirst.wlan[0].mib", "^.legacySecond.wlan[0].mib"}) { + auto *sta = check_and_cast(getModuleByPath(name)); + auto *ap = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + ASSERT(sta->hasPreparedLocalCapabilities() && !sta->isLocalHtCapable()); + ASSERT(ap->isLocalHtCapable() && ap->hasHtOperation()); + ASSERT(sta->hasActiveBss() == associated); + ASSERT(sta->getBssStationData().isAssociated == associated); + ASSERT(!sta->hasHtOperation()); + ASSERT(!sta->relationshipAllowsHt(ap->address)); + ASSERT(!ap->relationshipAllowsHt(sta->address)); + ASSERT(sta->findPeerCapabilities(ap->address) == nullptr); + ASSERT(ap->findPeerCapabilities(sta->address) == nullptr); + if (associated) { + ASSERT(sta->getBssData().bssid == ap->address); + ASSERT(sta->getBssData().ssid == ap->getBssData().ssid); + ASSERT(sta->hasPrimaryChannel() && sta->requirePrimaryChannel() == 6); + ASSERT(sta->getOperationBand() == ap->getOperationBand()); + ASSERT(ap->getPeerAssociationStatus(sta->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + } + else { + ASSERT(!sta->hasPrimaryChannel()); + ASSERT(ap->getBssAccessPointData().stations.count(sta->address) == 0); + } + } + } + virtual void initialize(int stage) override { if (stage == INITSTAGE_NETWORK_CONFIGURATION || stage == INITSTAGE_LAST) { + checkLegacyStations(true); auto configurator = check_and_cast(getModuleByPath("^.configurator")); auto apInterface = check_and_cast(getModuleByPath("^.ap.wlan[0]")); auto staInterface = check_and_cast(getModuleByPath("^.sta.wlan[0]")); @@ -41,22 +73,33 @@ class TestInitializationObserver : public cSimpleModule ASSERT(apMib->hasPrimaryChannel()); ASSERT(apMib->requirePrimaryChannel() == 6); if (stage == INITSTAGE_NETWORK_CONFIGURATION) { - ASSERT(apMib->bssData.ssid == "review-ssid"); - ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); + ASSERT(apMib->getBssData().ssid == "review-ssid"); + ASSERT(staMib->getBssData().ssid == apMib->getBssData().ssid); ASSERT(configurator->getTestWirelessId(apInterface) == configurator->getTestWirelessId(staInterface)); ASSERT(!staMib->address.isUnspecified()); - ASSERT(apMib->bssAccessPointData.stations.find(MacAddress::UNSPECIFIED_ADDRESS) == apMib->bssAccessPointData.stations.end()); - ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); - ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); - ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); + ASSERT(apMib->getBssAccessPointData().stations.find(MacAddress::UNSPECIFIED_ADDRESS) == apMib->getBssAccessPointData().stations.end()); + ASSERT(apMib->getBssAccessPointData().stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + auto *apFirst = check_and_cast(getModuleByPath("^.apFirst.wlan[0].mib")); + auto *staSecond = check_and_cast(getModuleByPath("^.staSecond.wlan[0].mib")); + ASSERT(staSecond->getBssData().ssid == apFirst->getBssData().ssid); + ASSERT(staSecond->findPeerHtState(apFirst->address) != nullptr); + ASSERT(apFirst->findPeerHtState(staSecond->address) != nullptr); + auto cache = apMib->findPeerHtState(staMib->address)->negotiatedCapabilities; + auto *provider = check_and_cast(apInterface->getSubmodule("mgmt")); + provider->prepareBss(); + provider->prepareBss(); + ASSERT(apMib->findPeerHtState(staMib->address)->negotiatedCapabilities == cache); + std::cout << "Both declaration orders and repeated BSS preparation verified.\n"; std::cout << "Simplified STA wireless identity available during network configuration.\n"; } else { ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); - ASSERT(apMib->findPeerHtState(staMib->address)->negotiatedCapabilities.operation.primaryChannel == 6); - ASSERT(staMib->findPeerHtState(apMib->address)->negotiatedCapabilities.operation.primaryChannel == 6); - std::cout << "Simplified STA HT peer state installed at last initialization stage.\n"; + ASSERT(apMib->getHtOperation().primaryChannel == 6); + ASSERT(staMib->getHtOperation().primaryChannel == 6); + std::cout << "Simplified STA HT peer state remains ready at last initialization stage.\n"; scheduleAt(SimTime(1500, SIMTIME_NS), new cMessage("checkShutdown")); scheduleAt(SimTime(2500, SIMTIME_NS), new cMessage("checkShutdownRestart")); scheduleAt(SimTime(3500, SIMTIME_NS), new cMessage("checkResolvableCrash")); @@ -73,53 +116,58 @@ class TestInitializationObserver : public cSimpleModule auto staInterface = check_and_cast(getModuleByPath("^.sta.wlan[0]")); auto apMib = check_and_cast(apInterface->getSubmodule("mib")); auto staMib = check_and_cast(staInterface->getSubmodule("mib")); + checkLegacyStations(strcmp(message->getName(), "checkShutdown") != 0 && + strcmp(message->getName(), "checkResolvableCrash") != 0); if (!strcmp(message->getName(), "checkShutdown")) { - ASSERT(!staMib->bssStationData.isAssociated); - ASSERT(apMib->bssAccessPointData.stations.find(staMib->address) == apMib->bssAccessPointData.stations.end()); + ASSERT(!staMib->getBssStationData().isAssociated); + ASSERT(apMib->getBssAccessPointData().stations.find(staMib->address) == apMib->getBssAccessPointData().stations.end()); ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); std::cout << "Simplified STA AP-side association and HT peer state removed after shutdown.\n"; } else if (!strcmp(message->getName(), "checkShutdownRestart")) { - ASSERT(staMib->bssStationData.isAssociated); - ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); - ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(staMib->getBssStationData().isAssociated); + ASSERT(staMib->getBssData().ssid == apMib->getBssData().ssid); + ASSERT(apMib->getBssAccessPointData().stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); std::cout << "Simplified STA association and HT peer state restored after shutdown restart.\n"; } else if (!strcmp(message->getName(), "checkResolvableCrash")) { - ASSERT(!staMib->bssStationData.isAssociated); - ASSERT(apMib->bssAccessPointData.stations.find(staMib->address) == apMib->bssAccessPointData.stations.end()); + ASSERT(!staMib->getBssStationData().isAssociated); + ASSERT(apMib->getBssAccessPointData().stations.find(staMib->address) == apMib->getBssAccessPointData().stations.end()); ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); std::cout << "Simplified STA AP-side association and HT peer state removed after resolvable crash.\n"; } else if (!strcmp(message->getName(), "checkCrashRestart")) { - ASSERT(staMib->bssStationData.isAssociated); - ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); - ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(staMib->getBssStationData().isAssociated); + ASSERT(staMib->getBssData().ssid == apMib->getBssData().ssid); + ASSERT(apMib->getBssAccessPointData().stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); std::cout << "Simplified STA association and HT peer state restored after crash restart.\n"; } else if (!strcmp(message->getName(), "prepareMissingApCrash")) { - ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->getBssAccessPointData().stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); - staMib->bssData.bssid = MacAddress("02:00:00:00:00:ff"); - ASSERT(staMib->bssData.bssid != apMib->address); - ASSERT(L3AddressResolver().findHostWithAddress(staMib->bssData.bssid) == nullptr); + auto operation = staMib->getHtOperation(); + staMib->commitBss(staMib->getBssData().ssid, MacAddress("02:00:00:00:00:ff"), + staMib->getOperationBand(), operation.primaryChannel, &operation); + ASSERT(staMib->getBssData().bssid != apMib->address); + ASSERT(L3AddressResolver().findHostWithAddress(staMib->getBssData().bssid) == nullptr); std::cout << "Simplified STA recorded BSSID made unresolvable before crash.\n"; } else if (!strcmp(message->getName(), "checkMissingApCrash")) { - ASSERT(!staMib->bssStationData.isAssociated); - ASSERT(staMib->bssData.bssid == MacAddress("02:00:00:00:00:ff")); - ASSERT(L3AddressResolver().findHostWithAddress(staMib->bssData.bssid) == nullptr); + ASSERT(!staMib->getBssStationData().isAssociated); + ASSERT(staMib->getBssData().bssid == MacAddress("02:00:00:00:00:ff")); + ASSERT(L3AddressResolver().findHostWithAddress(staMib->getBssData().bssid) == nullptr); ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); - ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->getBssAccessPointData().stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); std::cout << "Simplified STA crash tolerated missing AP lookup and cleared local HT peer state.\n"; + std::cout << "Legacy STAs preserve non-HT operation across both declaration orders and lifecycle transitions.\n"; } else ASSERT(false); @@ -158,6 +206,21 @@ network Ieee80211MgmtStaSimplifiedInitializationTestNetwork radioMedium: Ieee80211ScalarRadioMedium; configurator: TestIpv4NetworkConfigurator; scenarioManager: ScenarioManager; + legacyFirst: WirelessHost { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + apFirst: AccessPoint { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + wlan[*].agent.typename = ""; + } + staSecond: WirelessHost { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } sta: WirelessHost { parameters: wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; @@ -168,6 +231,11 @@ network Ieee80211MgmtStaSimplifiedInitializationTestNetwork wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; wlan[*].agent.typename = ""; } + legacySecond: WirelessHost { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } observer: TestInitializationObserver; } @@ -181,11 +249,18 @@ cmdenv-express-mode = true record-vector-results = false record-scalar-results = false +*.apFirst.wlan[0].address = "02:00:00:00:00:02" +*.apFirst.wlan[0].mgmt.ssid = "ap-first" +*.apFirst.wlan[0].radio.channelNumber = 6 +*.staSecond.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:02" *.ap.wlan[0].address = "02:00:00:00:00:01" *.sta.wlan[0].address = "auto" *.ap.wlan[0].mgmt.ssid = "review-ssid" *.ap.wlan[0].radio.channelNumber = 6 *.sta.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" +*.legacy*.wlan[0].opMode = "g(mixed)" +*.legacy*.wlan[0].bitrate = 54Mbps +*.legacy*.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" **.wlan[0].opMode = "n(mixed-2.4Ghz)" **.hasStatus = true **.scenarioManager.script = xmldoc("scenario.xml") @@ -205,15 +280,23 @@ record-scalar-results = false + + + + + + + + @@ -224,7 +307,7 @@ record-scalar-results = false Simplified STA wireless identity available during network configuration. %contains: stdout -Simplified STA HT peer state installed at last initialization stage. +Simplified STA HT peer state remains ready at last initialization stage. %contains: stdout Simplified STA AP-side association and HT peer state removed after shutdown. @@ -243,3 +326,9 @@ Simplified STA recorded BSSID made unresolvable before crash. %contains: stdout Simplified STA crash tolerated missing AP lookup and cleared local HT peer state. + +%contains: stdout +Both declaration orders and repeated BSS preparation verified. + +%contains: stdout +Legacy STAs preserve non-HT operation across both declaration orders and lifecycle transitions. diff --git a/tests/protocol/wifi/11n/WifiHtAssociation.test b/tests/protocol/wifi/11n/WifiHtAssociation.test new file mode 100644 index 00000000000..959b54b69ac --- /dev/null +++ b/tests/protocol/wifi/11n/WifiHtAssociation.test @@ -0,0 +1,41 @@ +%description: +HT infrastructure authentication/association sequence through the real packet PHY. +Uses the existing 802.11n configuration with two antennas and QoS enabled. +Local/peer HT constraints are checked separately by module tests. + +%file: WifiHtAssociation.cc + +#include "ProtocolTest.h" + +namespace inet { +namespace protocoltest { + +// Frame type (combined type+subtype byte): 0=Assoc Request, 1=Assoc Response, +// 8=Beacon, 11=Authentication. +Define_ProtocolTest(wifi_ht_association) +{ + return ProtocolTest("wifi_ht_association") + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 8").describe("a Beacon from the AP").within(2.0)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 11").describe("an Authentication request").within(2.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 11").describe("the Authentication response").within(2.0)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 0").describe("an Association Request").within(2.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 1").describe("the Association Response").within(2.0)); +} + +} // namespace protocoltest +} // namespace inet + +%inifile: test.ini + +[General] +*.tester.testName = "wifi_ht_association" +include ../../ini/_n.ini +include ../../ini/_base.ini + +%contains: stdout +PROTOCOLTEST wifi_ht_association: PASS diff --git a/tests/protocol/wifi/common/WifiDeauth.test b/tests/protocol/wifi/common/WifiDeauth.test index 4b569c7fa6e..80e188cf1c5 100644 --- a/tests/protocol/wifi/common/WifiDeauth.test +++ b/tests/protocol/wifi/common/WifiDeauth.test @@ -47,8 +47,8 @@ Define_ProtocolTest(wifi_deauth) .filterPacket("ieee80211mac.type == 8") .assertEvent([](const MatchContext& c) { auto mib = check_and_cast(c.event.node->getSubmodule("wlan", 0)->getSubmodule("mib")); - auto it = mib->bssAccessPointData.stations.find(MacAddress("02:00:00:00:00:03")); - return it != mib->bssAccessPointData.stations.end() && it->second == Ieee80211Mib::NOT_AUTHENTICATED; + auto it = mib->getBssAccessPointData().stations.find(MacAddress("02:00:00:00:00:03")); + return it != mib->getBssAccessPointData().stations.end() && it->second == Ieee80211Mib::NOT_AUTHENTICATED; }).describe("AP cleared the station authentication state").within(0.5)); } diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index dee6a669f79..2f479f16966 100644 --- a/tests/unit/Ieee80211HtCapabilities_1.test +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -35,7 +35,7 @@ Ieee80211HtOperation operation; operation.operatingChannelWidth = MHz(20); operation.basicMcsSupported[0] = true; -auto negotiated = negotiateHtCapabilities(local, peer, operation); +auto negotiated = negotiateHtCapabilities(local, peer); ASSERT(negotiated.localTxPeerRx.valid); ASSERT(negotiated.localRxPeerTx.valid); ASSERT(negotiated.localTxPeerRx.supportedMcs[0]); @@ -53,14 +53,14 @@ ASSERT(negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent == 1); // A 20 MHz-only peer may join a 20/40 MHz BSS. local.supportedChannelWidths.insert(MHz(40)); operation.operatingChannelWidth = MHz(40); -negotiated = negotiateHtCapabilities(local, peer, operation); +negotiated = negotiateHtCapabilities(local, peer); ASSERT(negotiated.localTxPeerRx.valid); ASSERT(negotiated.localTxPeerRx.supportedChannelWidths.count(MHz(20)) == 1); // Table 9-226 permits an undefined Tx MCS set; unknown is not "cannot transmit". peer.txMcsSetDefined = false; peer.txMcsNss = Ieee80211HtMcsNssMap(); -negotiated = negotiateHtCapabilities(local, peer, operation); +negotiated = negotiateHtCapabilities(local, peer); ASSERT(negotiated.localRxPeerTx.valid); operation.basicMcsSupported[1] = true; @@ -98,7 +98,7 @@ for (bool unequalModulation : {false, true}) { ASSERT(unequalRoundTrip.txRxMcsSetNotEqual); ASSERT(unequalRoundTrip.txMaxNss == 2); ASSERT(unequalRoundTrip.txUnequalModulation == unequalModulation); - auto unequalNegotiated = negotiateHtCapabilities(local, unequal, operation); + auto unequalNegotiated = negotiateHtCapabilities(local, unequal); ASSERT(unequalNegotiated.localRxPeerTx.valid); ASSERT(!unequalNegotiated.localRxPeerTx.supportedMcs[0]); } diff --git a/tests/unit/Ieee80211MibAssociationId_1.test b/tests/unit/Ieee80211MibAssociationId_1.test index aedebd81105..adb637a21c7 100644 --- a/tests/unit/Ieee80211MibAssociationId_1.test +++ b/tests/unit/Ieee80211MibAssociationId_1.test @@ -24,20 +24,20 @@ mib.cancelAssociationIdReservation(first); ASSERT(mib.reserveAssociationId(third) == 1); ASSERT(mib.commitAssociationId(second) == 2); -ASSERT(mib.bssAccessPointData.associationIds.at(second) == 2); +ASSERT(mib.getBssAccessPointData().associationIds.at(second) == 2); mib.cancelAssociationIdReservation(second); -ASSERT(mib.bssAccessPointData.associationIds.at(second) == 2); +ASSERT(mib.getBssAccessPointData().associationIds.at(second) == 2); ASSERT(mib.commitAssociationId(third) == 1); mib.releaseAssociationId(second); -ASSERT(mib.bssAccessPointData.associationIds.count(second) == 0); +ASSERT(mib.getBssAccessPointData().associationIds.count(second) == 0); ASSERT(mib.allocateAssociationId(fourth) == 2); mib.reserveAssociationId(first); -mib.bssAccessPointData.stations[first] = Ieee80211Mib::ASSOCIATED; +mib.setPeerAssociationStatus(first, Ieee80211Mib::ASSOCIATED); mib.clearAssociationIds(); -ASSERT(mib.bssAccessPointData.stations.empty()); -ASSERT(mib.bssAccessPointData.associationIds.empty()); +ASSERT(mib.getBssAccessPointData().stations.empty()); +ASSERT(mib.getBssAccessPointData().associationIds.empty()); ASSERT(mib.reserveAssociationId(second) == 1); EV << "Association ID ownership checks passed.\n"; diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index b472fd4e291..ab251b1f2e2 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -27,12 +27,18 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs return nullptr; } -static Ieee80211Mib::PeerHtState makePeerState(std::initializer_list mcsIndexes, +struct TestPeer : Ieee80211Mib::PeerHtState { + Ieee80211HtOperation operation; +}; + +static TestPeer makePeerState(std::initializer_list mcsIndexes, std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false) { - Ieee80211Mib::PeerHtState state; + TestPeer state; + auto capabilities = std::make_shared(); + state.negotiatedCapabilities = capabilities; state.valid = true; - auto& receiverCapabilities = state.negotiatedCapabilities.localTxPeerRx; + auto& receiverCapabilities = capabilities->localTxPeerRx; receiverCapabilities.valid = true; for (int mcsIndex : mcsIndexes) receiverCapabilities.supportedMcs[mcsIndex] = true; @@ -40,10 +46,16 @@ static Ieee80211Mib::PeerHtState makePeerState(std::initializer_list mcsInd receiverCapabilities.supportedChannelWidths.insert(bandwidth); receiverCapabilities.receiverShortGi20 = shortGi20; receiverCapabilities.receiverShortGi40 = shortGi40; - state.negotiatedCapabilities.operation.operatingChannelWidth = operatingChannelWidth; + state.operation.operatingChannelWidth = operatingChannelWidth; return state; } +static const IIeee80211Mode *selectForTest(const Ieee80211ModeSet *modeSet, + const TestPeer *state, const IIeee80211Mode *mode, const MacAddress& peer, bool eligible = true) +{ + return selectPeerCompatibleMode(modeSet, state, mode, peer, state ? &state->operation : nullptr, eligible); +} + %activity: const auto *modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); @@ -80,12 +92,12 @@ ASSERT(!modeSet->containsMode(modeOutsideSet)); const MacAddress peer("02:00:00:00:00:01"); auto shortGiPeer = makePeerState({8}, {MHz(20)}, MHz(20), true); -const auto *shortGiSelection = selectPeerCompatibleMode(modeSet, &shortGiPeer, mcs8Short, peer); +const auto *shortGiSelection = selectForTest(modeSet, &shortGiPeer, mcs8Short, peer); ASSERT(shortGiSelection == mcs8Short); ASSERT(shortGiSelection->isHtShortGuardInterval()); ASSERT(shortGiSelection->getDataMode()->getBandwidth() == MHz(20)); auto fortyMhzPeer = makePeerState({0}, {MHz(20), MHz(40)}, MHz(40), false, true); -const auto *fortyMhzSelection = selectPeerCompatibleMode(modeSet, &fortyMhzPeer, mcs0_40Short, peer); +const auto *fortyMhzSelection = selectForTest(modeSet, &fortyMhzPeer, mcs0_40Short, peer); ASSERT(fortyMhzSelection == mcs0_40Short); ASSERT(fortyMhzSelection->isHtShortGuardInterval()); ASSERT(fortyMhzSelection->getDataMode()->getBandwidth() == MHz(40)); @@ -93,16 +105,16 @@ ASSERT(fortyMhzSelection->getDataMode()->getBandwidth() == MHz(40)); // The complementary long-GI and short-GI variants are selectable when the // peer capabilities permit the corresponding exact mode. auto longGiMcs8Peer = makePeerState({8}, {MHz(20)}, MHz(20)); -ASSERT(selectPeerCompatibleMode(modeSet, &longGiMcs8Peer, mcs8Long, peer) == mcs8Long); +ASSERT(selectForTest(modeSet, &longGiMcs8Peer, mcs8Long, peer) == mcs8Long); auto shortGiMcs0Peer = makePeerState({0}, {MHz(20)}, MHz(20), true); -ASSERT(selectPeerCompatibleMode(modeSet, &shortGiMcs0Peer, mcs0Short, peer) == mcs0Short); +ASSERT(selectForTest(modeSet, &shortGiMcs0Peer, mcs0Short, peer) == mcs0Short); auto longGiFortyPeer = makePeerState({0}, {MHz(20), MHz(40)}, MHz(40)); -ASSERT(selectPeerCompatibleMode(modeSet, &longGiFortyPeer, mcs0_40Long, peer) == mcs0_40Long); +ASSERT(selectForTest(modeSet, &longGiFortyPeer, mcs0_40Long, peer) == mcs0_40Long); bool outsideModeRejected = false; try { auto compatiblePeer = makePeerState({0}, {MHz(20)}, MHz(20)); - selectPeerCompatibleMode(modeSet, &compatiblePeer, modeOutsideSet, peer); + selectForTest(modeSet, &compatiblePeer, modeOutsideSet, peer); } catch (const cRuntimeError&) { outsideModeRejected = true; @@ -112,58 +124,61 @@ ASSERT(outsideModeRejected); // The exact negotiated bitmap is not a contiguous maximum-MCS value: MCS 0 // and 2 are usable while MCS 1 is not (IEEE Std 802.11-2024, 10.6.5.8). auto sparsePeer = makePeerState({0, 2}, {MHz(20)}, MHz(20)); -ASSERT(selectPeerCompatibleMode(modeSet, &sparsePeer, mcs2Long, peer) == mcs2Long); -ASSERT(selectPeerCompatibleMode(modeSet, &sparsePeer, mcs1Long, peer) == mcs0Long); +ASSERT(selectForTest(modeSet, &sparsePeer, mcs2Long, peer) == mcs2Long); +ASSERT(selectForTest(modeSet, &sparsePeer, mcs1Long, peer) == mcs0Long); // A 20 MHz-only receiver cannot use a 40 MHz candidate, even when the BSS // itself permits 40 MHz operation. auto twentyOnlyPeer = makePeerState({0}, {MHz(20)}, MHz(40)); -const auto *twentyFallback = selectPeerCompatibleMode(modeSet, &twentyOnlyPeer, mcs0_40Short, peer); +const auto *twentyFallback = selectForTest(modeSet, &twentyOnlyPeer, mcs0_40Short, peer); ASSERT(twentyFallback == mcs0Long); ASSERT(twentyFallback->getDataMode()->getBandwidth() == MHz(20)); // The negotiated HT Operation width is an additional BSS-level limit over // the common capability widths. auto twentyOperationPeer = makePeerState({0}, {MHz(20), MHz(40)}, MHz(20)); -ASSERT(selectPeerCompatibleMode(modeSet, &twentyOperationPeer, mcs0_40Short, peer) == mcs0Long); +ASSERT(selectForTest(modeSet, &twentyOperationPeer, mcs0_40Short, peer) == mcs0Long); // A short-GI candidate falls back to the same-MCS long-GI variant when the // peer did not advertise the matching receiver capability bit. auto shortGiDisabledPeer = makePeerState({0, 8}, {MHz(20)}, MHz(20)); -const auto *shortGiFallback = selectPeerCompatibleMode(modeSet, &shortGiDisabledPeer, mcs8Short, peer); +const auto *shortGiFallback = selectForTest(modeSet, &shortGiDisabledPeer, mcs8Short, peer); ASSERT(shortGiFallback == mcs8Long); ASSERT(!shortGiFallback->isHtShortGuardInterval()); auto compatiblePeer = makePeerState({2}, {MHz(20)}, MHz(40)); -ASSERT(selectPeerCompatibleMode(modeSet, &compatiblePeer, mcs2Long, peer) == mcs2Long); +ASSERT(selectForTest(modeSet, &compatiblePeer, mcs2Long, peer) == mcs2Long); // Missing or invalid negotiated state keeps unicast HT selection on a legacy // operational fallback without increasing the caller's candidate bitrate. const auto *legacy12Mbps = modeSet->getMode(Mbps(12)); const auto *legacy6Mbps = modeSet->getMode(Mbps(6)); -ASSERT(selectPeerCompatibleMode(modeSet, nullptr, mcs2Long, peer) == legacy12Mbps); +ASSERT(selectForTest(modeSet, nullptr, mcs2Long, peer) == legacy12Mbps); +ASSERT(selectForTest(modeSet, &compatiblePeer, mcs2Long, peer, false) == legacy12Mbps); auto invalidPeer = makePeerState({2}, {MHz(20)}, MHz(20)); invalidPeer.valid = false; -ASSERT(selectPeerCompatibleMode(modeSet, &invalidPeer, mcs2Long, peer) == legacy12Mbps); +ASSERT(selectForTest(modeSet, &invalidPeer, mcs2Long, peer) == legacy12Mbps); auto invalidDirectionalPeer = makePeerState({2}, {MHz(20)}, MHz(20)); -invalidDirectionalPeer.negotiatedCapabilities.localTxPeerRx.valid = false; -ASSERT(selectPeerCompatibleMode(modeSet, &invalidDirectionalPeer, mcs2Long, peer) == legacy12Mbps); +auto invalidCapabilities = std::make_shared(*invalidDirectionalPeer.negotiatedCapabilities); +invalidCapabilities->localTxPeerRx.valid = false; +invalidDirectionalPeer.negotiatedCapabilities = invalidCapabilities; +ASSERT(selectForTest(modeSet, &invalidDirectionalPeer, mcs2Long, peer) == legacy12Mbps); -const auto *boundedMcs0Fallback = selectPeerCompatibleMode(modeSet, nullptr, mcs0Long, peer); +const auto *boundedMcs0Fallback = selectForTest(modeSet, nullptr, mcs0Long, peer); ASSERT(boundedMcs0Fallback == legacy6Mbps); ASSERT(boundedMcs0Fallback->getDataMode()->getNetBitrate() <= mcs0Long->getDataMode()->getNetBitrate()); // If no compatible HT mode is at or below the candidate bitrate, the result // is legacy rather than an HT mode that violates the negotiated bitmap. auto noHtFallbackPeer = makePeerState({76}, {MHz(20)}, MHz(20)); -const auto *noHtFallback = selectPeerCompatibleMode(modeSet, &noHtFallbackPeer, mcs0Long, peer); +const auto *noHtFallback = selectForTest(modeSet, &noHtFallbackPeer, mcs0Long, peer); ASSERT(noHtFallback == legacy6Mbps); ASSERT(noHtFallback->getHtMcsIndex() < 0); // Mode-set traversal and tie-breaks are stable and never depend on pointers. auto deterministicPeer = makePeerState({0, 2, 8}, {MHz(20)}, MHz(20)); -const auto *firstSelection = selectPeerCompatibleMode(modeSet, &deterministicPeer, mcs8Short, peer); -const auto *secondSelection = selectPeerCompatibleMode(modeSet, &deterministicPeer, mcs8Short, peer); +const auto *firstSelection = selectForTest(modeSet, &deterministicPeer, mcs8Short, peer); +const auto *secondSelection = selectForTest(modeSet, &deterministicPeer, mcs8Short, peer); ASSERT(firstSelection == secondSelection); ASSERT(firstSelection->getHtMcsIndex() == 8); ASSERT(!firstSelection->isHtShortGuardInterval()); From 8f147ea7b46b107f977ebd439527eb581f25bbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Fri, 18 Sep 2026 20:46:45 +0200 Subject: [PATCH 04/21] ieee80211: refactor: separate the read-only mode-set provider Catalog-only consumers should not require capability preparation. Introduce a paired C++/NED IIeee80211ModeSetProvider contract and make MAC configuration extend it with the preparation operation used by management. ModeSetModuleBase now depends only on the read-only provider. Migrate the replacement-provider fixture to the narrow contract, removing its throwing preparation stub. Update architecture, migration guidance and release notes to identify the contract custom catalog providers implement. Validation of the integrated changes: debug build, 5 focused module tests, 3 unit tests and 18 unchanged Wi-Fi/Ethernet/VLAN/configurator fingerprint cases pass. Scoped architecture checks pass; interface checking reports only the existing AV-CONTRACT-02 bodies. No recorded baseline changes. Plan: plan/done/80211htcapop-refactor-v3.md Change: src.ieee80211 | refactor | whatsnew migration | ieee80211-htcapop-v3 --- WHATSNEW | 7 ++--- .../design/ieee80211-model-architecture.md | 9 ++++--- doc/src/migration-guide/index.rst | 7 +++-- .../ieee80211/mac/common/ModeSetModuleBase.h | 4 +-- .../mac/contract/IIeee80211MacConfiguration.h | 9 +++---- .../contract/IIeee80211MacConfiguration.ned | 2 +- .../mac/contract/IIeee80211ModeSetProvider.h | 27 +++++++++++++++++++ .../contract/IIeee80211ModeSetProvider.ned | 13 +++++++++ .../Ieee80211ConfigurationContracts_1.test | 7 +++-- 9 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.ned diff --git a/WHATSNEW b/WHATSNEW index 9e29f5c5c19..822692b152c 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -290,9 +290,10 @@ Notable backward incompatible changes are the following: without accepting the HT operation advertised by an HT access point. Custom WLAN modules must replace ModeSetListener and modesetChanged with - an explicit modeSetModule provider, and replace direct MIB BSS/profile - writes with the owner APIs. Custom HT PHY contributors use typed capability - interfaces. The migration guide describes these source and wiring changes. + an explicit modeSetModule using IIeee80211ModeSetProvider, and replace + direct MIB BSS/profile writes with the owner APIs. Custom HT PHY contributors + use typed capability interfaces. The migration guide describes these source + and wiring changes. The selected legacy QoS and non-QoS ad hoc fingerprints remain unchanged; no recorded fingerprint baseline was updated. diff --git a/doc/project/design/ieee80211-model-architecture.md b/doc/project/design/ieee80211-model-architecture.md index 7df73468377..a119c0e6d17 100644 --- a/doc/project/design/ieee80211-model-architecture.md +++ b/doc/project/design/ieee80211-model-architecture.md @@ -93,10 +93,11 @@ Concrete signals declare their source/scope, change condition, payload, and life it creates no second writable authority. Commands, queries, and required coordination use typed calls or protocol messages, rather than notifications. -**Implemented HT contracts.** `IIeee80211MacConfiguration` exposes the configured catalog after -`LOCAL` and an idempotent `prepareLocalCapabilities()` operation after PHY readiness. MAC consumers -resolve their `modeSetModule` dependency through `ModeSetModuleBase` at `LINK_LAYER`; the MAC NED -provides the default descendant path. Management uses `macModule`. `LINK_LAYER` depends explicitly +**Implemented HT contracts.** `IIeee80211ModeSetProvider` exposes the configured catalog after +`LOCAL`. `IIeee80211MacConfiguration` extends it with an idempotent `prepareLocalCapabilities()` +operation after PHY readiness. MAC consumers resolve their `modeSetModule` dependency through the +narrow provider in `ModeSetModuleBase` at `LINK_LAYER`; the MAC NED provides the default descendant +path. Management uses `macModule`. `LINK_LAYER` depends explicitly on both `PHYSICAL_LAYER` and `NETWORK_INTERFACE_CONFIGURATION`, so addresses and contribution inputs are available before simplified association. Its AP preparation/install/removal calls use `IIeee80211BssProvider`; no remote `LAST` callback is required. diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index a8f16c4fee1..5ef0061b526 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -8,14 +8,17 @@ IEEE 802.11 Capability and BSS State Ownership -------------------------------------------- Custom modules that used ``ModeSetListener`` or subscribed to ``modesetChanged`` -must obtain the configured catalog through ``IIeee80211MacConfiguration``. +must obtain the configured catalog through ``IIeee80211ModeSetProvider``. For catalog consumers, derive from ``ModeSetModuleBase``, declare a ``modeSetModule`` NED parameter, and call the base initialization before using ``modeSet`` at ``INITSTAGE_LINK_LAYER``. Keep ``NUM_INIT_STAGES``. The built-in MAC supplies the descendant parameter default; standalone consumers must point -it at a module implementing the C++ and NED configuration contracts. Move +it at a module implementing the C++ and NED mode-set provider contracts. Move algorithm initialization formerly performed by the signal callback to that initialization stage. Ordinary catalog queries must not reset algorithm state. +Management uses ``IIeee80211MacConfiguration``, which extends the mode-set +provider with capability preparation; catalog-only providers need not implement +that operation. Custom transmitters and receivers that contribute HT capabilities implement ``IIeee80211TransmitterCapabilities`` and ``IIeee80211ReceiverCapabilities``, diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h index 2092283634e..47aca8ad69b 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h @@ -10,14 +10,14 @@ #include "inet/common/SimpleModule.h" #include "inet/common/ModuleRefByPar.h" -#include "inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h" +#include "inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" namespace inet::ieee80211 { /** Base for modules with a declared, configuration-lifetime catalog dependency. */ class INET_API ModeSetModuleBase : public SimpleModule { protected: - ModuleRefByPar modeSetProvider; + ModuleRefByPar modeSetProvider; const physicallayer::Ieee80211ModeSet *modeSet = nullptr; int numInitStages() const override { return NUM_INIT_STAGES; } void initialize(int stage) override; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h index 27b67801365..e7dcc142d6b 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h @@ -8,19 +8,16 @@ #ifndef INET_IIEEE80211MACCONFIGURATION_H #define INET_IIEEE80211MACCONFIGURATION_H -#include "inet/common/INETDefs.h" - -namespace inet::physicallayer { class Ieee80211ModeSet; } +#include "inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h" namespace inet::ieee80211 { -/** Configured catalog is ready after LOCAL; explicit preparation requires PHY readiness. +/** MAC configuration extends catalog access with preparation after PHY readiness. * Repeated preparation preserves protocol and algorithm state. */ -class INET_API IIeee80211MacConfiguration +class INET_API IIeee80211MacConfiguration : public IIeee80211ModeSetProvider { public: virtual ~IIeee80211MacConfiguration() = default; - [[nodiscard]] virtual const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const = 0; virtual void prepareLocalCapabilities() = 0; }; } // namespace inet::ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned index ed517dddcee..4bdadf22c67 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned +++ b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.ned @@ -7,6 +7,6 @@ package inet.linklayer.ieee80211.mac.contract; // Configured mode catalog and explicit capability preparation; see the C++ contract. -moduleinterface IIeee80211MacConfiguration +moduleinterface IIeee80211MacConfiguration extends IIeee80211ModeSetProvider { } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h new file mode 100644 index 00000000000..5646b2f9c16 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h @@ -0,0 +1,27 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef INET_IIEEE80211MODESETPROVIDER_H +#define INET_IIEEE80211MODESETPROVIDER_H + +#include "inet/common/INETDefs.h" + +namespace inet::physicallayer { class Ieee80211ModeSet; } + +namespace inet::ieee80211 { + +/** Read-only configured catalog, available after LOCAL and stable across stop/restart. */ +class INET_API IIeee80211ModeSetProvider +{ + public: + virtual ~IIeee80211ModeSetProvider() = default; + [[nodiscard]] virtual const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const = 0; +}; + +} // namespace inet::ieee80211 + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.ned b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.ned new file mode 100644 index 00000000000..5c008909b55 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.ned @@ -0,0 +1,13 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee80211.mac.contract; + +// Read-only configured mode catalog; see the C++ contract. +moduleinterface IIeee80211ModeSetProvider +{ +} diff --git a/tests/module/Ieee80211ConfigurationContracts_1.test b/tests/module/Ieee80211ConfigurationContracts_1.test index 5b59c0fa6e7..ae36857923f 100644 --- a/tests/module/Ieee80211ConfigurationContracts_1.test +++ b/tests/module/Ieee80211ConfigurationContracts_1.test @@ -45,14 +45,13 @@ class OfflineExtUpper : public ExtInterface }; Define_Module(OfflineExtUpper); -class TestCatalogProvider : public cSimpleModule, public IIeee80211MacConfiguration +class TestCatalogProvider : public cSimpleModule, public IIeee80211ModeSetProvider { protected: const Ieee80211ModeSet *catalog = nullptr; virtual void initialize() override { catalog = Ieee80211ModeSet::getModeSet("g(mixed)"); } public: virtual const Ieee80211ModeSet *getConfiguredModeSet() const override { return catalog; } - virtual void prepareLocalCapabilities() override { throw cRuntimeError("Catalog-only consumer must not prepare HT"); } }; Define_Module(TestCatalogProvider); @@ -127,11 +126,11 @@ import inet.common.SimpleModule; import inet.emulation.linklayer.ieee80211.ExtUpperIeee80211Interface; import inet.common.scenario.ScenarioManager; import inet.node.inet.AdhocHost; -import inet.linklayer.ieee80211.mac.contract.IIeee80211MacConfiguration; +import inet.linklayer.ieee80211.mac.contract.IIeee80211ModeSetProvider; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; simple OfflineTap { parameters: @class(::OfflineTap); gates: input lowerLayerIn; output lowerLayerOut; } module OfflineExtUpper extends ExtUpperIeee80211Interface { parameters: @class(::OfflineExtUpper); } -simple TestCatalogProvider like IIeee80211MacConfiguration { parameters: @class(::TestCatalogProvider); } +simple TestCatalogProvider like IIeee80211ModeSetProvider { parameters: @class(::TestCatalogProvider); } simple TestCatalogConsumer extends SimpleModule { parameters: @class(::TestCatalogConsumer); From 97898f840a8d2f6fa6043814386676155759124a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:00:56 +0200 Subject: [PATCH 05/21] ieee80211: format: remove redundant blank lines Remove redundant blank lines from the affected IEEE 802.11 sources so subsequent functional changes contain no incidental whitespace cleanup. Change: src.ieee80211 | format | - --- 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/Ieee80211Transmitter.cc | 1 - 6 files changed, 6 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index bb147edcd19..a990caf486b 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/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 fbd2d7165f592564e7f616c971e96540bcaa4289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:08:40 +0200 Subject: [PATCH 06/21] ieee80211: add: require explicit data-mode guard interval queries Rate selection needs to distinguish equal-bitrate modes with different guard intervals. Require an explicit data-mode query, retaining a negative sentinel for PHYs without one. Reject unknown VHT guard-interval types instead of interpreting them as short GI. Document the external API. Change: src.ieee80211 | behavior.add | test whatsnew migration --- 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.cc | 10 ++++++++++ .../wireless/ieee80211/mode/Ieee80211VhtMode.h | 1 + 12 files changed, 50 insertions(+) diff --git a/WHATSNEW b/WHATSNEW index 822692b152c..34430146dfa 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -297,6 +297,12 @@ Notable backward incompatible changes are the following: The selected legacy QoS and non-QoS ad hoc fingerprints remain unchanged; no recorded fingerprint baseline was updated. +17. 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 5ef0061b526..6f832489a79 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -105,6 +105,20 @@ answers; :cpp:`Arp` shows how. An implementation that resolves addresses without packets has nobody to ask. It overrides the method with an empty body, as :cpp:`GlobalArp` does, and the client then takes the address. +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.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index e5736766c5a..ad18da33cdd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -312,6 +312,16 @@ bps Ieee80211VhtModeBase::getGrossBitrate() const return grossBitrate; } +const simtime_t Ieee80211VhtDataMode::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 Ieee80211VhtModeBase::getNumberOfDataSubcarriers() const { if (bandwidth == MHz(20)) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index aa4aa3639b0..943aa538077 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; virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; From 3eadb65dc17ad88e96594ef05723f31ab283c5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:13:55 +0200 Subject: [PATCH 07/21] ieee80211: fix: correct HT MCS 32, 73 and 76 definitions The optional HT 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. Direct optional-MCS assertions cover stream count, per-stream modulation, bitrate for both guard intervals and long-GI symbol duration; these entries are not exercised by the selectable MCS 0-31 catalog. Change: src.ieee80211.Ieee80211HtmcsTable | behavior.change.fix | test whatsnew --- WHATSNEW | 3 ++ .../ieee80211/mode/Ieee80211HtMode.cc | 12 ++++-- tests/unit/Ieee80211HtModeSet_1.test | 37 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 34430146dfa..b46dafef4a0 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -357,6 +357,9 @@ Notable backward compatible changes are the following: ResidenceTimeTag and, with it, the FlowTag and the PacketEventTag of the packet. Those two tags now survive the measurement. +6. 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..a1eb2cb5d29 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-41: 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));}); diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 44877fd9d68..b11e1f4e714 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -5,6 +5,7 @@ Also verify exact sparse HT MCS and width derivation from typed mode entries. %includes: #include +#include #include #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" @@ -40,6 +41,42 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs %activity: +// IEEE Std 802.11-2024, Tables 19-35, 19-38 and 19-41. These optional +// table entries are not part of the selectable MCS 0-31 catalog below. +struct OptionalMcsExpectation { + const Ieee80211Htmcs *mcs; + int streams; + int bitsPerSubcarrier[4]; + double grossMbps; + double netMbps; + int symbolsFor1000Bits; +}; +const OptionalMcsExpectation optionalMcs[] = { + {&Ieee80211HtmcsTable::htMcs32BW40MHz, 1, {1, 0, 0, 0}, 12, 6, 43}, + {&Ieee80211HtmcsTable::htMcs73BW40MHz, 4, {6, 6, 4, 2}, 486, 364.5, 1}, + {&Ieee80211HtmcsTable::htMcs76BW20MHz, 4, {6, 6, 6, 4}, 286, 214.5, 2}, +}; +for (const auto& expected : optionalMcs) { + const Ieee80211OfdmModulation *streams[] = {expected.mcs->getModulation(), + expected.mcs->getStreamExtension1Modulation(), expected.mcs->getStreamExtension2Modulation(), + expected.mcs->getStreamExtension3Modulation()}; + for (int i = 0; i < 4; i++) + ASSERT((streams[i] ? streams[i]->getSubcarrierModulation()->getCodeWordSize() : 0) == expected.bitsPerSubcarrier[i]); + for (auto gi : {Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT}) { + Ieee80211HtDataMode data(expected.mcs, expected.mcs->getBandwidth(), gi); + bool shortGi = gi == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT; + auto symbol = SimTime(shortGi ? 3600 : 4000, SIMTIME_NS); + double rateScale = shortGi ? 10.0 / 9 : 1; + ASSERT(data.getNumberOfSpatialStreams() == expected.streams); + ASSERT(std::abs(data.getGrossBitrate().get() - expected.grossMbps * 1e6 * rateScale) < 1); + ASSERT(std::abs(data.getNetBitrate().get() - expected.netMbps * 1e6 * rateScale) < 1); + if (!shortGi) { + ASSERT(data.getSymbolInterval() == symbol); + ASSERT(data.getDuration(b(1000)) == expected.symbolsFor1000Bits * symbol); + } + } +} + const auto *htModeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); const auto *vhtOnlyModeSet = Ieee80211ModeSet::getModeSet("ac"); From f3679f7ad99507f31d05320b738459299c4bd045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:14:35 +0200 Subject: [PATCH 08/21] ieee80211: fix: distinguish band and preamble in mode caches Band and preamble affect mode behavior but were absent from cache identity. Include them in HT/VHT cache keys and reject unsupported VHT greenfield requests before cache lookup, including after mixed-format cache use. Change: src.ieee80211 | behavior.change.fix | test whatsnew --- .../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 a1eb2cb5d29..4deb4a8eb56 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 ad18da33cdd..74fd61aa14f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -682,9 +682,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; @@ -703,7 +707,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 943aa538077..aa284d70473 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 39aa78b4e2bee20aaaeedda63e5e0b778f767a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:15:55 +0200 Subject: [PATCH 09/21] ieee80211: fix: 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. Duration-component operations remain pure virtual on IIeee80211Mode; Ieee80211ModeBase supplies the shared defaults, with HT/VHT overrides retaining their format-specific calculations. Document the implementation contract for external modes. Plan: plan/done/ht-gi-devin-comment-closure.md Change: src.ieee80211 | behavior.change.fix | test whatsnew migration --- WHATSNEW | 11 +++++- doc/src/migration-guide/index.rst | 5 +++ .../Ieee80211LayeredOfdmTransmitter.cc | 2 +- .../wireless/ieee80211/mode/IIeee80211Mode.h | 5 +++ .../ieee80211/mode/Ieee80211HtMode.cc | 34 +++++++++++++++---- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 8 +++-- .../ieee80211/mode/Ieee80211ModeBase.h | 3 ++ .../ieee80211/mode/Ieee80211VhtMode.cc | 30 ++++++++++++---- .../ieee80211/mode/Ieee80211VhtMode.h | 12 +++++-- .../packetlevel/Ieee80211Transmitter.cc | 6 ++-- tests/unit/Ieee80211HtGuardInterval_1.test | 24 +++++++++++++ tests/unit/Ieee80211HtModeSet_1.test | 6 ++-- 12 files changed, 121 insertions(+), 25 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index b46dafef4a0..c72971fda7c 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -359,7 +359,16 @@ Notable backward compatible changes are the following: 6. 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. + + IIeee80211Mode duration-component queries are now pure virtual. Their + previous defaults are supplied by Ieee80211ModeBase; direct external + implementations must provide these queries or inherit the base defaults. + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 6f832489a79..1e8a505292e 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -108,6 +108,11 @@ as :cpp:`GlobalArp` does, and the client then takes the address. Migrating IEEE 802.11 PHY Modes ------------------------------ +``IIeee80211Mode::getPreambleDuration()``, ``getHeaderDuration()`` and +``getDataDuration(b)`` are now pure virtual. Direct interface implementations +must implement them, or inherit ``Ieee80211ModeBase`` for the previous defaults. +Existing HT/VHT overrides retain their format-specific timing behavior. + External implementations of ``IIeee80211DataMode`` must now implement the pure virtual guard-interval query: 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..64064fb19f1 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 = 0; + virtual const simtime_t getHeaderDuration() const = 0; + virtual const simtime_t getDataDuration(b dataLength) const = 0; 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 4deb4a8eb56..f74a0a7433b 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/Ieee80211ModeBase.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h index dd5d6fdd0ca..d3387f0a934 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h @@ -23,6 +23,9 @@ class INET_API Ieee80211ModeBase : public IIeee80211Mode virtual int getHtMcsIndex() const override { return -1; } virtual bool isHtShortGuardInterval() const override { return false; } virtual const char *getName() const override { return name.c_str(); } + virtual const simtime_t getPreambleDuration() const override { return getPreambleMode()->getDuration(); } + virtual const simtime_t getHeaderDuration() const override { return getHeaderMode()->getDuration(); } + virtual const simtime_t getDataDuration(b dataLength) const override { return getDuration(dataLength) - getPreambleDuration() - getHeaderDuration(); } }; } /* namespace physicallayer */ diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index 74fd61aa14f..bd553b35dae 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 @@ -649,6 +653,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 aa284d70473..7cc8b91ef3d 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; - 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"; diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index b11e1f4e714..ec7a3bb230b 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -70,10 +70,8 @@ for (const auto& expected : optionalMcs) { ASSERT(data.getNumberOfSpatialStreams() == expected.streams); ASSERT(std::abs(data.getGrossBitrate().get() - expected.grossMbps * 1e6 * rateScale) < 1); ASSERT(std::abs(data.getNetBitrate().get() - expected.netMbps * 1e6 * rateScale) < 1); - if (!shortGi) { - ASSERT(data.getSymbolInterval() == symbol); - ASSERT(data.getDuration(b(1000)) == expected.symbolsFor1000Bits * symbol); - } + ASSERT(data.getSymbolInterval() == symbol); + ASSERT(data.getDuration(b(1000)) == expected.symbolsFor1000Bits * symbol); } } From 1d4e1010957f3d57b8c0e0a146e8e7f0fd5bc539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:22:26 +0200 Subject: [PATCH 10/21] ieee80211: add+change: complete guard-interval mode lookup Equal-rate catalog entries can differ in GI and are not distinct adaptation steps. Qualify bitrate lookups by GI and traverse adjacent rates by bitrate. Compatibility remapping preserves exact bitrate, bandwidth, NSS and GI; GI absence is an exact tuple property, not the public lookup wildcard. Cover exact versus tolerant matching and GI presence in both directions. Change: src.ieee80211.Ieee80211ModeSet | behavior.add+change | test whatsnew migration --- WHATSNEW | 5 +- doc/src/migration-guide/index.rst | 6 + .../ieee80211/mode/Ieee80211ModeSet.cc | 122 +++++++--- .../ieee80211/mode/Ieee80211ModeSet.h | 14 +- tests/unit/Ieee80211HtGuardInterval_1.test | 218 ++++++++++++++++++ tests/unit/Ieee80211HtModeSet_1.test | 21 ++ tests/unit/Ieee80211PeerModeSelection_1.test | 4 + 7 files changed, 354 insertions(+), 36 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index c72971fda7c..d60a400f7b6 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -301,7 +301,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 1e8a505292e..fb0a9bf246a 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -124,6 +124,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..906910a00e2 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,49 @@ 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(); + 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); + // 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 (candidateDataMode->getNetBitrate() == sourceBitrate && + 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 +715,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 +748,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 +797,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..7f66eb5c566 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; } +}; + %activity: // Reject unsupported VHT greenfield before any mixed-format cache request. bool rejectedVhtGreenfieldBeforeMixed = false; @@ -34,6 +50,160 @@ catch (cRuntimeError&) { } ASSERT(rejectedVhtGreenfieldBeforeMixed); +const auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +ASSERT(modeSet->getNumModes() == 135); + +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); +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()); + +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 +217,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 +242,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/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index ec7a3bb230b..00a6bc4967c 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -39,6 +39,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: // IEEE Std 802.11-2024, Tables 19-35, 19-38 and 19-41. These optional @@ -104,6 +113,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))); diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index ab251b1f2e2..aa7a8ea4b89 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -146,6 +146,10 @@ const auto *shortGiFallback = selectForTest(modeSet, &shortGiDisabledPeer, mcs8S ASSERT(shortGiFallback == mcs8Long); ASSERT(!shortGiFallback->isHtShortGuardInterval()); +auto shortGiDisabledOnlyMcs0Peer = makePeerState({0}, {MHz(20)}, MHz(20)); +const auto *shortGiMcs0Fallback = selectForTest(modeSet, &shortGiDisabledOnlyMcs0Peer, mcs8Short, peer); +ASSERT(shortGiMcs0Fallback == mcs0Long); + auto compatiblePeer = makePeerState({2}, {MHz(20)}, MHz(40)); ASSERT(selectForTest(modeSet, &compatiblePeer, mcs2Long, peer) == mcs2Long); From 446257e7ae2dcf4d864b189da2571fb9eb0101f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:27:03 +0200 Subject: [PATCH 11/21] ieee80211: add: 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 selectors. Negative values leave GI unspecified; explicit values must match the modeled interval. Change: src.ieee80211 | behavior.add | test whatsnew --- WHATSNEW | 4 ++++ .../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, 14 insertions(+), 8 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index d60a400f7b6..9e862d907d5 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -372,6 +372,10 @@ Notable backward compatible changes are the following: previous defaults are supplied by Ieee80211ModeBase; direct external implementations must provide these queries or inherit the base defaults. + 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 953ceb5599c..17de0e44db9 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -31,7 +31,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"); @@ -57,7 +57,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 5cb9f9bff87..35f9aa93181 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -33,8 +33,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 20b9522c46e..984713ff256 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 2015d0669f8..31f8effbe5e 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -28,8 +28,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 654cce6c9e8572ac9be89695b4237f857a7a1579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:28:29 +0200 Subject: [PATCH 12/21] ieee80211: fix: select mandatory response rates by bitrate A mandatory response can use the same bitrate as a nonmandatory request. Select the highest mandatory bitrate at or below the request instead of stepping strictly to a lower rate. Change: src.ieee80211.RateSelection | behavior.change.fix | test whatsnew --- .../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 984713ff256..33052cfc4e8 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 b42a3e101cce0614315a16c57a58f3c6fd7ada4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:29:02 +0200 Subject: [PATCH 13/21] ieee80211: fix: use legacy basic rates for group-addressed frames Mixed HT catalogs place mandatory HT entries above legacy rates, so the fastest mandatory mode makes Beacons invisible to legacy stations. Both selectors must use a mandatory legacy operational mode when the basic legacy set is nonempty (IEEE 802.11-2024, 10.6.5.4). Preserve eligible configured basic rates, including equivalent external mode tuples. Otherwise select the fastest basic legacy rate. Unit checks cover membership, equivalent tuples and fallback; mixed-HT discovery checks legacy association through both DCF and HCF access points. The group-rate restriction changes broadcast airtime in these run-0 configurations, whose CSV and matching JSON expectations travel here: - examples/wireless/lan80211ac: Ping1; - 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. For example, a configured 54 Mbps group rate falls back to 24 Mbps, changing airtime and the subsequent event trajectory. The expectations are measured on parent cbbba4d487, which includes corrected wire encoding and beacon scheduling. Bypassing only selectGroupAddressedMode restores that parent's fingerprints for the ten showcase/tutorial configurations, while ordinary production execution matches the corrected values. Retain existing limits, run numbers and fingerprint ingredients. PingRtt, analogmodel/Routing and Noise, and configurator/Step8A and Step8B retain the parent values: no group-rate baseline movement is observed in those five controls. The Ping1 expectations here describe group-rate selection before the subsequent VHT peer-negotiation change. The STA discovery regression checks this same rule: an HT association still uses a legacy basic mode for multicast, while unicast remains HT-eligible. Plan: plan/done/ht-gi-devin-comment-closure.md Change: src.ieee80211 | behavior.change.fix | test whatsnew migration fingerprint --- .../Ieee80211PeerModeSelection.cc | 17 +++ .../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 | 74 ++++----- tests/fingerprint/tutorials.csv | 5 +- tests/module/Ieee80211MgmtStaDiscovery_1.test | 12 +- tests/module/Ieee80211MixedHtDiscovery_1.test | 141 ++++++++++++++++++ tests/unit/Ieee80211GroupModeSelection_1.test | 52 +++++++ 11 files changed, 289 insertions(+), 106 deletions(-) create mode 100644 tests/module/Ieee80211MixedHtDiscovery_1.test create mode 100644 tests/unit/Ieee80211GroupModeSelection_1.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 78eb9e886f2..5dfaf17d0bd 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -14,6 +14,23 @@ 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 auto *resolvedMode = modeSet->containsMode(requestedMode) ? requestedMode : modeSet->findCompatibleMode(requestedMode); + const IIeee80211Mode *legacyMode = nullptr; + for (const auto *candidate : modeSet->getLegacyOperationalModes()) { + if (!modeSet->getIsMandatory(candidate)) + continue; + if (candidate == resolvedMode) + 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 cd9bebfaf8c..6c6543576cf 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 17de0e44db9..dfdceea5ade 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -151,9 +151,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()); @@ -164,46 +170,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 33052cfc4e8..f800af00820 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 64abe9fcac0..387c0c467ac 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, e618-be99/tplx;edb3-6679/~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, dca6-3751/tplx;a44c-17bf/~tNl;96ff-ed7c/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac /examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 1560-028f/tplx;e6db-28c0/~tNl;805b-8a25/~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;66de-a722/~tND;7a22-c289/tyf, PASS, wireless Ipv4 diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 330453edd7b..94747bc82f5 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;4893-f8fa/~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;cb45-7a00/~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, 7fb8-49dc/tplx;2f60-762d/~tNl;0437-f151/~tND;07bb-3973/tyf, PASS, wireless /showcases/visualizer/canvas/ieee80211/, -f omnetpp.ini -c SignalLevels -r 0, 100s, 51ec-d359/tplx;cec5-d464/~tNl;a0c6-b121/~tND;c099-e5c5/tyf, PASS, Ipv4 -/showcases/visualizer/canvas/submoduleinfo/, -f omnetpp.ini -c PacketCounts -r 0, 5s, 271c-6821/tplx;3c0c-db82/~tNl;8315-9723/~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;8315-9723/~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;57f0-e9ab/~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;57f0-e9ab/~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;85f0-a9c3/~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;df7a-beae/~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;87e2-d13d/~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;0a60-f71c/~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;d600-2b73/~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;ee1b-5f7a/~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;ee1b-5f7a/~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;3bb2-9797/~tND;bd88-5ba6/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Routing -r 0, 5s, 56b0-3510/tplx;baeb-0d2c/~tNl;3d4b-65fc/~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;7b7c-2bfb/~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;9e70-fbb5/~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;d680-b8ad/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 /showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 5b0b-f055/tplx;8511-cc76/~tNl;3402-f804/~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;1403-6fb2/~tND, PASS, wireless Ipv4 +/showcases/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 21cb-ec85/tplx;30b0-ee3e/~tNl;ae94-6216/~tND, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 26be-ff05/tplx;9c39-00a3/~tNl;03a9-2f02/~tND;093e-1ca4/tyf, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 8d7c-5091/tplx;84a6-b893/~tNl;cb18-5b4d/~tND;bda9-15d1/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 7a6b-0096/tplx;cd89-6ad5/~tNl;3d32-d9e4/~tND;093e-1ca4/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 904d-389e/tplx;21aa-5c22/~tNl;6b58-22a3/~tND;bda9-15d1/tyf, PASS, wireless Ipv4 /showcases/wireless/ratecontrol/, -f omnetpp.ini -c NoRateControl -r 0, 14s, 7ee9-503a/tplx;0816-e58f/~tNl;ec30-e32d/~tND;dad3-7f89/tyf, PASS, wireless Ipv4 /showcases/wireless/ratecontrol/, -f omnetpp.ini -c AarfRateControl -r 0, 12s, a7bc-05bb/tplx;9de0-4dd3/~tNl;de90-6575/~tND;7539-d32d/tyf, PASS, wireless Ipv4 diff --git a/tests/fingerprint/store.json b/tests/fingerprint/store.json index 321b6b5c23a..6b257bb7e61 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": "cb45-7a00", "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": "d600-2b73", "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": "57f0-e9ab", "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": "57f0-e9ab", "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": "9e70-fbb5", "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": "ae94-6216", "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": "7a6b-0096", "timestamp": 1681992799.6971657, "itervars": "$repetition==0" }, @@ -46327,7 +46327,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "eb84-1083", + "fingerprint": "3d32-d9e4", "timestamp": 1681992799.697302, "itervars": "$repetition==0" }, @@ -46339,7 +46339,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "1061-2f07", + "fingerprint": "cd89-6ad5", "timestamp": 1681992799.6972342, "itervars": "$repetition==0" }, @@ -46351,7 +46351,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "1a49-72b3", + "fingerprint": "904d-389e", "timestamp": 1681992799.6976697, "itervars": "$repetition==0" }, @@ -46375,7 +46375,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "83cc-bdf2", + "fingerprint": "6b58-22a3", "timestamp": 1681992799.697813, "itervars": "$repetition==0" }, @@ -46387,7 +46387,7 @@ "sim_time_limit": "10s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "9760-e96a", + "fingerprint": "21aa-5c22", "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": "02d8-d7df", "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": "3e25-000b", "timestamp": 1681992799.808365, "itervars": "$repetition==0" }, @@ -50827,7 +50827,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "c423-d956", + "fingerprint": "82f2-2fe2", "timestamp": 1681992799.8087497, "itervars": "$repetition==0" }, @@ -50839,7 +50839,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "1374-346a", + "fingerprint": "ca9f-eb38", "timestamp": 1681992799.8085785, "itervars": "$repetition==0" }, @@ -53747,4 +53747,4 @@ "timestamp": 1681992799.829868, "itervars": "$repetition==0" } -] \ No newline at end of file +] diff --git a/tests/fingerprint/tutorials.csv b/tests/fingerprint/tutorials.csv index 609c4b97949..9e0cfea3ea4 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, 0b7e-7adc/tplx;12a6-f004/~tNl;8b42-bf7b/~tND;d3f3-ed4c/tyf, PASS, wireless EthernetMac Ipv4 # Step8A extended /tutorials/configurator/, -f omnetpp.ini -c Step8B -r 0, 100s, b1ac-8912/tplx;12a6-f004/~tNl;96e2-030b/~tND;af16-1150/tyf, PASS, wireless EthernetMac Ipv4 -/tutorials/configurator/, -f omnetpp.ini -c Step9 -r 0, 100s, 70cf-424a/tplx;d39c-9f1f/~tNl;c4ee-f7ca/~tND;c38c-98ef/tyf, PASS, wireless EthernetMac Ipv4 +/tutorials/configurator/, -f omnetpp.ini -c Step9 -r 0, 100s, 3e25-000b/tplx;ca9f-eb38/~tNl;82f2-2fe2/~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;707b-bb40/~tND;f078-56fe/tyf, PASS, wireless Ipv4 +/tutorials/configurator/, -f omnetpp.ini -c Step10C -r 0, 100s, e443-0781/tplx;cc4d-d519/~tNl;02d8-d7df/~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, e75f-2a09/tplx;d302-92c4/~tNl;b67b-3f1f/~tND;4b03-e5a1/tyf, PASS, wireless EthernetMac Ipv4 @@ -200,4 +200,3 @@ /tutorials/queueing/, -f omnetpp.ini -c RequestResponse -r 0, 100s, 7d97-5413/tplx;0000-0000/~tNl;0000-0000/~tND;1aeb-2f3a/tyf, PASS, queueing /tutorials/queueing/, -f omnetpp.ini -c Telnet -r 0, 100s, f176-070e/tplx;0000-0000/~tNl;0000-0000/~tND;43fd-14e6/tyf, PASS, queueing /tutorials/queueing/, -f omnetpp.ini -c ExampleNetwork -r 0, 10s, 3783-f5ed/tplx;0000-0000/~tNl;0000-0000/~tND;5a45-9aba/tyf, PASS, queueing - diff --git a/tests/module/Ieee80211MgmtStaDiscovery_1.test b/tests/module/Ieee80211MgmtStaDiscovery_1.test index fc4650abf71..4a4d1a69bf6 100644 --- a/tests/module/Ieee80211MgmtStaDiscovery_1.test +++ b/tests/module/Ieee80211MgmtStaDiscovery_1.test @@ -511,9 +511,9 @@ class Ieee80211MgmtStaDiscoveryTest : public cSimpleModule, public cListener ASSERT(dcfSparseMode->getHtMcsIndex() == 0); ASSERT(dcfSparseMode->getDataMode()->getBandwidth() == MHz(20)); - // The invalid/absent negotiated state is a legacy-only unicast - // condition, while group-addressed traffic keeps its existing HT - // selection policy. + // Invalid/absent negotiated state forces legacy unicast. Group traffic + // independently uses the nonempty basic legacy set (IEEE Std + // 802.11-2024, 10.6.5.4), regardless of this peer's negotiated state. staMib->removePeerHtCapabilities(address); const auto *dcfLegacyMode = dcfRateSelection->computeMode(&ratePacket, dataHeader); const auto *qosLegacyMode = qosRateSelection->computeMode(&ratePacket, dataHeader, nullptr); @@ -543,8 +543,10 @@ class Ieee80211MgmtStaDiscoveryTest : public cSimpleModule, public cListener multicastHeader->setReceiverAddress(MacAddress::BROADCAST_ADDRESS); const auto *dcfMulticastMode = dcfRateSelection->computeMode(&ratePacket, multicastHeader); const auto *qosMulticastMode = qosRateSelection->computeMode(&ratePacket, multicastHeader, nullptr); - ASSERT(dcfMulticastMode->getHtMcsIndex() >= 0); - ASSERT(qosMulticastMode->getHtMcsIndex() >= 0); + ASSERT(dcfMulticastMode == modeSet->getMode(Mbps(24))); + ASSERT(qosMulticastMode == dcfMulticastMode); + ASSERT(dcfMulticastMode->getHtMcsIndex() < 0); + ASSERT(modeSet->getIsMandatory(dcfMulticastMode)); staMib->setPeerHtCapabilities(address, staMib->getLocalHtCapabilities()); ASSERT(dcfRateSelection->computeMode(&ratePacket, dataHeader)->getHtMcsIndex() >= 0); ASSERT(qosRateSelection->computeMode(&ratePacket, dataHeader, nullptr)->getHtMcsIndex() >= 0); diff --git a/tests/module/Ieee80211MixedHtDiscovery_1.test b/tests/module/Ieee80211MixedHtDiscovery_1.test new file mode 100644 index 00000000000..5a2069e048d --- /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->isLocalHtCapable()); + ASSERT(!sta->isLocalHtCapable()); + ASSERT(sta->getBssStationData().isAssociated); + ASSERT(sta->getBssData().bssid == ap->address); + ASSERT(ap->getBssAccessPointData().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. diff --git a/tests/unit/Ieee80211GroupModeSelection_1.test b/tests/unit/Ieee80211GroupModeSelection_1.test new file mode 100644 index 00000000000..8a0697e76ad --- /dev/null +++ b/tests/unit/Ieee80211GroupModeSelection_1.test @@ -0,0 +1,52 @@ +%description: +Group selection preserves an equivalent external basic legacy mode, while +retaining active-catalog identity and the existing fastest-basic fallback. + +%includes: +#include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +%global: +class GroupModeSet : public Ieee80211ModeSet +{ + public: + GroupModeSet(const std::vector& entries, const IIeee80211Mode *reference) : + Ieee80211ModeSet("group", entries, reference, PhyType::OFDM, false) {} +}; + +%activity: +const auto *catalog = Ieee80211ModeSet::getModeSet("a"); +const auto *six = check_and_cast(catalog->getMode(Mbps(6))); +const auto *twelve = catalog->getMode(Mbps(12)); +const auto *twentyFour = catalog->getMode(Mbps(24)); +const auto *nine = catalog->getMode(Mbps(9)); +// Copy the mode wrapper only; its immutable constituent modes are borrowed. +Ieee80211OfdmMode externalSix(*six); +GroupModeSet basic({{true, six, true}, {true, twelve, true}, {true, twentyFour, true}, {false, nine, true}}, six); +ASSERT(!basic.containsMode(&externalSix)); +ASSERT(basic.findCompatibleMode(&externalSix) == six); +ASSERT(selectGroupAddressedMode(&basic, &externalSix) == six); +ASSERT(selectGroupAddressedMode(&basic, six) == six); +ASSERT(selectGroupAddressedMode(&basic, twelve) == twelve); +ASSERT(selectGroupAddressedMode(&basic, nine) == twentyFour); +ASSERT(selectGroupAddressedMode(&basic, catalog->getMode(Mbps(54))) == twentyFour); +ASSERT(selectGroupAddressedMode(&basic, nullptr) == twentyFour); +// A compatible non-basic or non-operational entry must not become a basic rate. +GroupModeSet optional({{false, six, true}, {true, twentyFour, true}}, twentyFour); +ASSERT(selectGroupAddressedMode(&optional, &externalSix) == twentyFour); +GroupModeSet nonOperational({{true, six, false}, {true, twentyFour, true}}, twentyFour); +ASSERT(selectGroupAddressedMode(&nonOperational, &externalSix) == twentyFour); +// Exact membership wins even if an earlier equal tuple has different eligibility. +GroupModeSet duplicate({{false, &externalSix, true}, {true, six, true}, {true, twentyFour, true}}, six); +ASSERT(selectGroupAddressedMode(&duplicate, six) == six); +GroupModeSet noBasic({{false, six, true}}, six); +ASSERT(selectGroupAddressedMode(&noBasic, &externalSix) == &externalSix); +ASSERT(selectGroupAddressedMode(&noBasic, nullptr) == nullptr); +std::cout << "Group mode compatibility and fallback verified.\n"; + +%contains: stdout +Group mode compatibility and fallback verified. From 3651b1674b2eaf426bac3ca14dd7cd0685a0bf96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:51:47 +0200 Subject: [PATCH 14/21] ieee80211: add+change: coordinate mode-set transitions Runtime catalog changes must complete before observers see the new state. Coordinate them through the MAC simple module and declared provider paths; the containing interface supplies wiring without owning protocol state. Apply local capabilities, management operation and dependent algorithms in that order. Reject missing membership before PHY mutation; failures after mutation remain fatal without rollback. Keep ordinary preparation idempotent and reuse HT compatibility caches when their capability inputs are unchanged. Explicit profile replacement refreshes those results while retaining relationship-scoped peer advertisements; selection still checks current eligibility and BSS operation. Reapplying the same catalog neither resets algorithms nor publishes a catalog change. Initialization uses typed queries rather than a mode-set broadcast. Refresh fixed rates, advertisements and active contention on changed catalogs. Retain whole remaining backoff slots and bounded retry windows without a new random draw. Resolve default TXOP limits at each start while preserving active TXOPs and configured overrides. Cover replacement providers, registration, retained frames, AP edge channels, both radio setters and fatal failure paths. Preflight catalog-only transmitter compatibility before setting either transition guard. Rejecting a 40 MHz HT-to-legacy request leaves state and notifications unchanged, allowing an explicit compatible-mode retry. Reuse the read-only resolver in the transmitter setter and preserve virtual setter dispatch; failures after PHY mutation remain fatal. Plan: plan/done/ht-gi-devin-comment-closure.md Change: src.ieee80211 | behavior.add+change | test whatsnew migration --- WHATSNEW | 16 + .../design/ieee80211-model-architecture.md | 15 + doc/src/migration-guide/index.rst | 43 +++ src/inet/common/Simsignals.cc | 3 +- src/inet/common/Simsignals.h | 3 +- .../ieee80211/Ieee80211Interface.ned | 2 +- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 97 +++++- .../linklayer/ieee80211/mac/Ieee80211Mac.h | 18 +- .../linklayer/ieee80211/mac/Ieee80211Mac.ned | 2 +- .../ieee80211/mac/channelaccess/Dcaf.cc | 12 +- .../ieee80211/mac/channelaccess/Dcaf.h | 3 + .../ieee80211/mac/channelaccess/Edcaf.cc | 12 +- .../ieee80211/mac/channelaccess/Edcaf.h | 3 + .../ieee80211/mac/common/ModeSetModuleBase.cc | 11 +- .../ieee80211/mac/common/ModeSetModuleBase.h | 7 +- .../ieee80211/mac/contention/Contention.cc | 30 ++ .../ieee80211/mac/contention/Contention.h | 2 + .../ieee80211/mac/contract/IContention.h | 4 + .../ieee80211/mac/originator/TxopProcedure.cc | 10 +- .../ieee80211/mac/originator/TxopProcedure.h | 1 + .../mac/ratecontrol/RateControlBase.cc | 8 +- .../mac/ratecontrol/RateControlBase.h | 1 + .../mac/rateselection/QosRateSelection.cc | 65 ++-- .../mac/rateselection/QosRateSelection.h | 5 +- .../mac/rateselection/RateSelection.cc | 50 ++- .../mac/rateselection/RateSelection.h | 7 +- .../ieee80211/mgmt/Ieee80211MgmtApBase.cc | 3 + .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 18 +- .../ieee80211/mgmt/Ieee80211MgmtBase.h | 9 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 17 + .../linklayer/ieee80211/mib/Ieee80211Mib.h | 2 + .../IIeee80211ModeSetCoordinator.h | 41 +++ .../IIeee80211ModeSetCoordinator.ned | 14 + .../packetlevel/IIeee80211ModeSetListener.h | 35 ++ .../contract/packetlevel/IIeee80211Radio.h | 32 ++ .../contract/packetlevel/IIeee80211Radio.ned | 14 + .../packetlevel/Ieee80211ControlInfo.msg | 6 +- .../ieee80211/packetlevel/Ieee80211Radio.cc | 72 +++- .../ieee80211/packetlevel/Ieee80211Radio.h | 15 +- .../ieee80211/packetlevel/Ieee80211Radio.ned | 5 +- .../ieee80211/packetlevel/Ieee80211Receiver.h | 1 + .../packetlevel/Ieee80211Transmitter.cc | 29 +- .../packetlevel/Ieee80211Transmitter.h | 8 + .../Ieee80211AlternativeRadioStartup_1.test | 84 +++++ .../module/Ieee80211ContentionModeSet_1.test | 210 ++++++++++++ .../Ieee80211MgmtApChannelChange_1.test | 36 +- .../Ieee80211MgmtApUnavailableChannel_1.test | 5 +- tests/module/Ieee80211ModeSetFailure_1.test | 131 ++++++++ .../Ieee80211ModeSetRegistration_1.test | 262 +++++++++++++++ tests/module/Ieee80211ModeSetRetry_1.test | 307 +++++++++++++++++ .../module/Ieee80211ModeSetTransition_1.test | 314 ++++++++++++++++++ tests/module/Ieee80211TxopModeSet_1.test | 98 ++++++ tests/unit/Ieee80211HtGuardInterval_1.test | 279 ++++++++++++++++ 53 files changed, 2396 insertions(+), 81 deletions(-) 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/IIeee80211ModeSetListener.h 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/Ieee80211AlternativeRadioStartup_1.test create mode 100644 tests/module/Ieee80211ContentionModeSet_1.test create mode 100644 tests/module/Ieee80211ModeSetFailure_1.test create mode 100644 tests/module/Ieee80211ModeSetRegistration_1.test create mode 100644 tests/module/Ieee80211ModeSetRetry_1.test create mode 100644 tests/module/Ieee80211ModeSetTransition_1.test create mode 100644 tests/module/Ieee80211TxopModeSet_1.test diff --git a/WHATSNEW b/WHATSNEW index 9e862d907d5..0d3e4bb460d 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -375,6 +375,22 @@ 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. + The MAC now coordinates explicitly registered mode-set consumers, + updating MAC, management, rate selection, and active contention before + publishing modesetChanged from the MAC. Initialization queries the catalog; + unchanged catalogs retain algorithm state. Incompatible catalog-only requests + are rejected before opening a transition, allowing an explicit-mode retry. + Failures after PHY mutation remain fatal. External PHY + and contention implementations have new contracts described in the + migration guide. Default TXOP limits are resolved for each new TXOP after + a mode-set change; active TXOPs and configured overrides retain their limits. + + 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/project/design/ieee80211-model-architecture.md b/doc/project/design/ieee80211-model-architecture.md index a119c0e6d17..28ce6c8deeb 100644 --- a/doc/project/design/ieee80211-model-architecture.md +++ b/doc/project/design/ieee80211-model-architecture.md @@ -118,3 +118,18 @@ operation. The current ad hoc no-beacon abstraction likewise has no learned chan accepted peer advertisements. Stop/crash clears operational relationships while retaining prepared configuration. Physical AP channel context is retained by management for restart; STA operation is learned from accepted management information, independently of scan tuning. + +**Runtime catalog reconfiguration.** The MAC simple module implements the typed +`IIeee80211ModeSetCoordinator` contract. The radio resolves its coordinator through +`modeSetCoordinatorModule`; the containing interface only supplies default wiring. +Consumers register through their declared catalog/configuration provider when it supports +coordination. A read-only replacement provider need not implement that optional runtime role. + +An explicit changed-catalog transaction refreshes the MAC-assembled capability profile, +then management's local operation and dependent algorithms. HT compatibility results are +replaced only when capability inputs change; accepted peer knowledge remains relationship-scoped +and selection still checks eligibility and current operation. Reapplying the same catalog +does not reset algorithms. Ordinary preparation and stop/restart retain their existing contracts. +The MAC publishes `modesetChanged` only after a changed runtime catalog is applied; initialization +uses typed queries without a catalog notification. Membership changes and reentrant transitions +are rejected during application/publication. Failures after PHY mutation are fatal, without rollback. diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index fb0a9bf246a..a07f1dcaa82 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -130,6 +130,49 @@ 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`` implements ``IIeee80211ModeSetCoordinator`` for explicit runtime +catalog reconfiguration. The radio's ``modeSetCoordinatorModule`` points to the +MAC by default; a custom composition can provide another typed coordinator. +``ModeSetModuleBase`` registers derived-state consumers through the configured +catalog provider when that provider also supports coordination. Read-only +replacement providers remain usable without a coordinator. Management registers +through ``macModule`` in ``MANAGEMENT_STATE``. Preserve base initialization and +put required updates in ``applyModeSet()``, not notification callbacks. + +The MAC updates local capabilities, management updates its operation, and derived +consumers update their state before completion is published. Ordinary preparation +remains idempotent. Explicit profile replacement refreshes directional capability +caches only when their inputs change; operation changes do not rebuild those +caches. Peer information remains scoped to its relationship, with selection gated +by current eligibility and operation. Stop/restart retains prepared configuration. + +Observe ``modesetChanged`` from the MAC after a changed runtime catalog has been +applied. Initialization queries the configured catalog without a notification; +reapplying the same catalog does not reset algorithms or publish a catalog change. +The borrowed mode-set payload is immutable. Observers never finish the transition. +Standalone radios without a coordinator publish their own notification. Duplicate +registration is idempotent; late registration and membership changes during a +transition are rejected. Detach a consumer before deleting it. Participant or +observer exceptions remain fatal; partially applied changes cannot be resumed. + +Catalog-only changes now check transmitter compatibility before opening the +coordinated transition. After catching an incompatible-mode error, callers can +retry with ``setModeSetAndMode()`` and a valid explicit mode. Custom transmitter +implementations can override the non-mutating ``computeModeForModeSet()`` query +to match their catalog-only setter's resolution policy; it must reject an +unresolvable request without changing state. + +HT capability assembly queries the typed transmitter and receiver contributions +described above. Management obtains channel/band context from the PHY. Generic +legacy radios do not require HT contribution contracts. + +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/common/Simsignals.cc b/src/inet/common/Simsignals.cc index e79367a3d83..c588aa820f5 100644 --- a/src/inet/common/Simsignals.cc +++ b/src/inet/common/Simsignals.cc @@ -21,6 +21,8 @@ simsignal_t l2ApDisassociatedSignal = cComponent::registerSignal("l2ApDisassocia simsignal_t linkBrokenSignal = cComponent::registerSignal("linkBroken"); +simsignal_t modesetChangedSignal = cComponent::registerSignal("modesetChanged"); + simsignal_t interpacketGapStartedSignal = cComponent::registerSignal("interpacketGapStarted"); simsignal_t interpacketGapEndedSignal = cComponent::registerSignal("interpacketGapEnded"); @@ -152,4 +154,3 @@ void printSignalBanner(simsignal_t signalID, intval_t value, const cObject *deta } } // namespace inet - diff --git a/src/inet/common/Simsignals.h b/src/inet/common/Simsignals.h index 3301001fe46..b6b20c0f461 100644 --- a/src/inet/common/Simsignals.h +++ b/src/inet/common/Simsignals.h @@ -30,6 +30,8 @@ extern INET_API simsignal_t // admin linkBrokenSignal, // used for manet link layer feedback + modesetChangedSignal, + interpacketGapStartedSignal, interpacketGapEndedSignal, @@ -151,4 +153,3 @@ void printSignalBanner(simsignal_t signalID, intval_t value, const cObject *deta } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned index e32eaba59fc..97b87db8034 100644 --- a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned @@ -73,6 +73,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(".mac")); *.macModule = default(absPath(".mac")); *.mibModule = default(absPath(".mib")); *.interfaceTableModule = default(absPath(this.interfaceTableModule)); @@ -129,4 +130,3 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface classifier.in <-- { @display("m=n"); } <-- upperLayerIn; } - diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index da8dd51225e..f48b185a9ac 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" @@ -26,6 +27,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/contract/IIeee80211ReceiverCapabilities.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -58,6 +60,7 @@ 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()); } else if (stage == INITSTAGE_LINK_LAYER) { cModule *llcModule = gate("upperLayerOut")->getNextGate()->getOwnerModule(); @@ -81,6 +84,8 @@ void Ieee80211Mac::initialize(int stage) if (mib->qos && !hcf) throw cRuntimeError("Missing hcf module, required for QoS"); } + else if (stage == INITSTAGE_NETWORK_CONFIGURATION) + modeSetInitialized = true; } void Ieee80211Mac::prepareLocalCapabilities() @@ -88,8 +93,16 @@ void Ieee80211Mac::prepareLocalCapabilities() Enter_Method("prepareLocalCapabilities"); if (mib->hasPreparedLocalCapabilities()) return; + updateLocalHtCapabilities(); +} + +void Ieee80211Mac::updateLocalHtCapabilities(bool reconfiguration) +{ if (!modeSet->isHtOperationSupported()) { - mib->installLocalHtCapabilities(Ieee80211HtCapabilities(), false); + if (reconfiguration) + mib->reconfigureLocalHtCapabilities(Ieee80211HtCapabilities(), false); + else + mib->installLocalHtCapabilities(Ieee80211HtCapabilities(), false); return; } auto *configuredRadio = check_and_cast(gate("lowerLayerOut")->getNextGate()->getOwnerModule()); @@ -142,7 +155,10 @@ void Ieee80211Mac::prepareLocalCapabilities() if (localHtCapabilities.maxAmpduLengthExponent < 0 || localHtCapabilities.maxAmpduLengthExponent > 3) throw cRuntimeError("htMaxAmpduLengthExponent must be between 0 and 3"); - mib->installLocalHtCapabilities(localHtCapabilities, true); + if (reconfiguration) + mib->reconfigureLocalHtCapabilities(localHtCapabilities, true); + else + mib->installLocalHtCapabilities(localHtCapabilities, true); } void Ieee80211Mac::initializeRadioMode() @@ -394,6 +410,83 @@ 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)); + // Mode-set application uses the coordinator contract, not notifications. +} + +void Ieee80211Mac::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateLocalHtCapabilities(true); +} + +void Ieee80211Mac::registerModeSetConsumer(cModule *consumer, Phase phase) +{ + Enter_Method_Silent(); + if (changingModeSet || modeSetInitialized) + throw cRuntimeError("Mode-set consumers must register before initialization completes"); + if (consumer == nullptr || consumer == this || getContainingNicModule(consumer) != getContainingNicModule(this) || + dynamic_cast(consumer) == nullptr) + throw cRuntimeError("Mode-set consumer must implement the transition contract"); + if (phase != MANAGEMENT_STATE && phase != DERIVED_STATE) + throw cRuntimeError("Invalid mode-set consumer 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 Ieee80211Mac::unregisterModeSetConsumer(cModule *consumer) +{ + Enter_Method_Silent(); + if (changingModeSet) + throw cRuntimeError("Cannot detach a mode-set consumer during a transition"); + if (consumer == nullptr) + throw cRuntimeError("Cannot detach a null mode-set consumer"); + if (getContainingNicModule(consumer) != getContainingNicModule(this)) + throw cRuntimeError("Cannot detach a mode-set consumer from another interface"); + modeSetConsumers.erase(consumer->getId()); +} + +void Ieee80211Mac::beginModeSetChange(const Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + if (changingModeSet) + throw cRuntimeError("Reentrant interface mode-set change"); + if (!modeSetInitialized || !mib->hasPreparedLocalCapabilities()) + throw cRuntimeError("Cannot reconfigure before mode-set initialization completes"); + if (newModeSet == nullptr) + throw cRuntimeError("Cannot clear the mode set of an IEEE 802.11 interface"); + if (std::none_of(modeSetConsumers.begin(), modeSetConsumers.end(), [](const auto& entry) { return entry.second == MANAGEMENT_STATE; })) + throw cRuntimeError("Required management mode-set consumer is not registered"); + for (const auto& entry : modeSetConsumers) + if (getSimulation()->getModule(entry.first) == nullptr) + throw cRuntimeError("Mode-set consumer was deleted without unregistering"); + pendingModeSet = newModeSet; + changingModeSet = true; +} + +void Ieee80211Mac::completeModeSetChange(const Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + if (!changingModeSet || pendingModeSet != newModeSet) + throw cRuntimeError("Mode-set completion does not match the pending transition"); + if (modeSet != newModeSet) { + applyModeSet(newModeSet); + for (auto phase : {MANAGEMENT_STATE, DERIVED_STATE}) + for (const auto& entry : modeSetConsumers) + if (entry.second == phase) + check_and_cast( + getSimulation()->getModule(entry.first))->applyModeSet(newModeSet); + mib->publishStateChange(); + emit(modesetChangedSignal, const_cast(modeSet)); + } + pendingModeSet = nullptr; + changingModeSet = false; +} + 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 09b69439f3a..65b89e247d7 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -22,6 +22,8 @@ #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" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" namespace inet { namespace ieee80211 { @@ -36,13 +38,24 @@ 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, public IIeee80211MacConfiguration +class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfiguration, + public physicallayer::IIeee80211ModeSetListener, public physicallayer::IIeee80211ModeSetCoordinator { public: static simsignal_t frameTransmissionOutcomeSignal; + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + void registerModeSetConsumer(cModule *consumer, Phase phase) override; + void unregisterModeSetConsumer(cModule *consumer) override; + void beginModeSetChange(const physicallayer::Ieee80211ModeSet *modeSet) override; + void completeModeSetChange(const physicallayer::Ieee80211ModeSet *modeSet) override; protected: FcsMode fcsMode; + std::map modeSetConsumers; + const physicallayer::Ieee80211ModeSet *pendingModeSet = nullptr; + bool changingModeSet = false; + bool modeSetInitialized = false; ModuleRefByPar mib; opp_component_ptr llc; @@ -66,8 +79,10 @@ class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfig virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; virtual void initializeRadioMode(); + void updateLocalHtCapabilities(bool reconfiguration = false); 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; @@ -119,4 +134,3 @@ class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfig } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned index bd2f9b93915..09c308a0695 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned @@ -94,6 +94,7 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac, IIeee80211MacCon @display("i=block/layer"); @class(Ieee80211Mac); + @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); // Completed runtime transition; observational only @signal[linkBroken](type=inet::Packet); // TODO this signal is only present for the statistic to pass the signal check, to be removed @signal[frameTransmissionOutcome](type=inet::Packet); @statistic[packetSentToUpper](title="packets sent to upper layer"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @@ -137,4 +138,3 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac, IIeee80211MacCon @display("p=250,200"); } } - diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 71b157c6e77..5b3c93abf82 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -62,7 +62,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; } @@ -119,7 +120,14 @@ void Dcaf::expectedChannelAccess(simtime_t time) // don't care } +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 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h index 29fa80fb55e..2ff03b4eb25 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h @@ -19,6 +19,9 @@ namespace ieee80211 { class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetModuleBase { + 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/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index b0070f3c401..fad741bbc8d 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -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; } @@ -186,7 +187,14 @@ int Edcaf::getCwMin(AccessCategory ac, int aCwMin) } } +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 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index 4e984fb03a0..2a5f6262398 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 ModeSetModuleBase { + 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/ModeSetModuleBase.cc b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc index 77406a7179e..462874a9611 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc @@ -6,15 +6,24 @@ #include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h" namespace inet::ieee80211 { void ModeSetModuleBase::initialize(int stage) { - if (stage == INITSTAGE_LOCAL) + if (stage == INITSTAGE_LOCAL) { modeSetProvider.reference(this, "modeSetModule", true); + if (auto coordinator = dynamic_cast(modeSetProvider.get())) + coordinator->registerModeSetConsumer(this, physicallayer::IIeee80211ModeSetCoordinator::DERIVED_STATE); + } else if (stage == INITSTAGE_LINK_LAYER) { modeSet = modeSetProvider->getConfiguredModeSet(); if (modeSet == nullptr) throw cRuntimeError("Configured IEEE 802.11 mode catalog is unavailable"); } } +void ModeSetModuleBase::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = newModeSet; +} } // namespace inet::ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h index 47aca8ad69b..c0850f34b94 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h @@ -12,10 +12,15 @@ #include "inet/common/ModuleRefByPar.h" #include "inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet::ieee80211 { /** Base for modules with a declared, configuration-lifetime catalog dependency. */ -class INET_API ModeSetModuleBase : public SimpleModule +class INET_API ModeSetModuleBase : public SimpleModule, public physicallayer::IIeee80211ModeSetListener { + public: + const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + void applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) override; + protected: ModuleRefByPar modeSetProvider; const physicallayer::Ieee80211ModeSet *modeSet = nullptr; 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/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc index 1b15a6ee6b2..a85e98c3680 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc @@ -22,7 +22,8 @@ void TxopProcedure::initialize(int stage) { ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { - limit = par("txopLimit"); + configuredLimit = par("txopLimit"); + limit = configuredLimit; WATCH(start); WATCH(protectionMechanism); } @@ -78,9 +79,12 @@ void TxopProcedure::startTxop(AccessCategory ac) Enter_Method("startTxop"); if (start != -1) throw cRuntimeError("Txop is already running"); - if (limit == -1) { + // Resolve the default for each new TXOP; a mode change must not alter + // the limit of a TXOP already in progress or a configured override. + if (configuredLimit == -1) limit = getTxopLimit(modeSet->getPhyType(), ac).get(); - } + else + limit = configuredLimit; // The STA selects between single and multiple protection when it transmits the first frame of a TXOP. // All subsequent frames transmitted by the STA in the same TXOP use the same class of duration settings. protectionMechanism = selectProtectionMechanism(ac); diff --git a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h index 820dd6dea8b..c37a58a6e83 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.h @@ -33,6 +33,7 @@ class INET_API TxopProcedure : public ModeSetModuleBase protected: simtime_t start = -1; + simtime_t configuredLimit = -1; simtime_t limit = -1; ProtectionMechanism protectionMechanism = ProtectionMechanism::UNDEFINED_PROTECTION; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index f0d161ab4c3..2ae0237638a 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -62,7 +62,13 @@ void RateControlBase::emitDatarateChangedSignal(const MacAddress& receiver, cons } } +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 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index 5eb2fa2abf2..d4072b5fe81 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 ModeSetModuleBase, 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 dfdceea5ade..532812e5902 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -28,20 +28,7 @@ void QosRateSelection::initialize(int stage) if (stage == INITSTAGE_LINK_LAYER) { fastestMandatoryMode = modeSet->getFastestMandatoryMode(); 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(); } } @@ -66,6 +53,31 @@ void QosRateSelection::ensurePerReceiverModesResolved() } } +void QosRateSelection::updateModes() +{ + if (modeSet == nullptr) + return; + fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + 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(); @@ -102,10 +114,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"); } @@ -121,10 +131,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"); } @@ -240,6 +248,14 @@ const IIeee80211Mode *QosRateSelection::computeMode(Packet *packet, const PtrgetReceiverAddress(), computeControlFrameMode(header, txopProcedure)); } +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) { @@ -249,6 +265,10 @@ void QosRateSelection::frameTransmitted(Packet *packet, const PtrgetHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerCapabilities(peerAddress), mode, peerAddress, @@ -257,4 +277,3 @@ const IIeee80211Mode *QosRateSelection::getPeerCompatibleMode(const MacAddress& } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index c84beacc05c..60b353138f0 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -32,6 +32,9 @@ namespace ieee80211 { */ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetModuleBase { + public: + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; @@ -57,6 +60,7 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetModule protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) 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 @@ -90,4 +94,3 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetModule } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index f800af00820..648e1378e8c 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" @@ -32,19 +33,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 +73,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(); @@ -184,6 +195,14 @@ const IIeee80211Mode *RateSelection::computeMode(Packet *packet, const Ptr(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); +} void RateSelection::frameTransmitted(Packet *packet, const Ptr& header) { @@ -212,6 +231,10 @@ void RateSelection::emitDatarateSelected(cComponent *emitter, const PtrgetHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerCapabilities(peerAddress), mode, peerAddress, @@ -220,4 +243,3 @@ const IIeee80211Mode *RateSelection::getPeerCompatibleMode(const MacAddress& pee } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 3c407bcf694..a1ea7a92cf6 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 { @@ -33,6 +34,10 @@ namespace ieee80211 { */ class INET_API RateSelection : public IRateSelection, public ModeSetModuleBase { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; @@ -57,6 +62,7 @@ class INET_API RateSelection : public IRateSelection, public ModeSetModuleBase protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) 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 @@ -98,4 +104,3 @@ class INET_API RateSelection : public IRateSelection, public ModeSetModuleBase } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc index 8a1f7ef1f1a..3c5a90ba0ce 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc @@ -18,6 +18,9 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211RadioChannelChangedDetails.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" namespace inet { diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 4ff5f53563e..cbcd3191a80 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" @@ -35,6 +36,8 @@ void Ieee80211MgmtBase::initialize(int stage) numMgmtFramesReceived = 0; numMgmtFramesDropped = 0; configurationProvider.reference(this, "macModule", true); + if (auto coordinator = dynamic_cast(configurationProvider.get())) + coordinator->registerModeSetConsumer(this, IIeee80211ModeSetCoordinator::MANAGEMENT_STATE); WATCH(numMgmtFramesReceived); WATCH(numMgmtFramesDropped); } @@ -55,6 +58,20 @@ void Ieee80211MgmtBase::prepareConfiguration() if (modeSet == nullptr) throw cRuntimeError("Configured IEEE 802.11 mode catalog is unavailable"); configurationPrepared = true; + updateSupportedRates(); +} + +void Ieee80211MgmtBase::applyModeSet(const Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = newModeSet; + updateSupportedRates(); + if (isUp() && mib->hasActiveBss()) + prepareLocalOperation(); +} + +void Ieee80211MgmtBase::updateSupportedRates() +{ supportedRates = Ieee80211SupportedRatesElement(); extendedSupportedRates = Ieee80211ExtendedSupportedRatesElement(); int rateIndex = 0; @@ -240,4 +257,3 @@ void Ieee80211MgmtBase::stop() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index 9527212cd75..9ff181c18d0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -20,6 +20,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 { @@ -29,8 +30,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; @@ -50,6 +55,7 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; void prepareConfiguration(); + void updateSupportedRates(); /** Dispatches incoming messages to handleTimer(), handleUpperMessage() or processFrame(). */ virtual void handleMessageWhenUp(cMessage *msg) override; @@ -130,4 +136,3 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index a990caf486b..0a68b50c469 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -178,6 +178,23 @@ const Ieee80211Mib::PeerHtState *Ieee80211Mib::findPeerCapabilities(const MacAdd return it == peerHtStates.end() || !it->second.valid ? nullptr : &it->second; } +void Ieee80211Mib::reconfigureLocalHtCapabilities(const Ieee80211HtCapabilities& capabilities, bool htSupported) +{ + checkStateMutation(); + if (!localCapabilitiesPrepared) + throw cRuntimeError("Cannot reconfigure unprepared local HT capabilities"); + if (localHtCapabilities == capabilities && localHtCapabilitiesValid == htSupported) + return; + localHtCapabilities = capabilities; + localHtCapabilitiesValid = htSupported; + for (auto& entry : peerHtStates) { + if (entry.second.valid) + entry.second.negotiatedCapabilities = std::make_shared( + negotiateHtCapabilities(localHtCapabilities, entry.second.advertisedCapabilities)); + } + stateChangePending = true; +} + bool Ieee80211Mib::relationshipAllowsHt(const MacAddress& address) const { const auto *peer = findPeerCapabilities(address); diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index c5622a85a02..e048dbf9682 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -122,6 +122,8 @@ class INET_API Ieee80211Mib : public SimpleModule void clearAssociationIds(); // Initialization/preparation only. A changed profile requires inactive BSS and no peers. void installLocalHtCapabilities(const Ieee80211HtCapabilities& capabilities, bool htSupported); + // Explicit coordinated reconfiguration; ordinary preparation remains guarded. + void reconfigureLocalHtCapabilities(const Ieee80211HtCapabilities& capabilities, bool htSupported); bool hasPreparedLocalCapabilities() const { return localCapabilitiesPrepared; } bool isLocalHtCapable() const { return localHtCapabilitiesValid; } bool hasActiveBss() const { return bssActive; } 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..90b15b79cf5 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetCoordinator.h @@ -0,0 +1,41 @@ +// +// 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. + * The coordinator updates MAC capabilities before MANAGEMENT_STATE and DERIVED_STATE; + * consumers within DERIVED_STATE are independent. MAC_STATE is reserved for the owner. + * 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, MANAGEMENT_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 new file mode 100644 index 00000000000..420b58cea0c --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h @@ -0,0 +1,35 @@ +// +// 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; + +/** + * 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. + */ +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/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/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 000551f3cfa..7b4211a78a1 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -8,8 +8,13 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211RadioChannelChangedDetails.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" @@ -44,6 +49,7 @@ void Ieee80211Radio::initialize(int stage) FlatRadioBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { + modeSetCoordinator.reference(this, "modeSetCoordinatorModule", false); const char *fcsModeString = par("fcsMode"); fcsMode = parseFcsMode(fcsModeString, true); } @@ -68,13 +74,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) @@ -92,13 +102,55 @@ 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)); + // Reject incompatible catalog-only requests before either transition guard is set. + // Keep the setter dispatch below; failures after PHY mutation remain fatal. + if (!explicitMode) + transmitter->computeModeForModeSet(modeSet); + if (modeSetCoordinator != nullptr) + modeSetCoordinator->beginModeSetChange(modeSet); + changingModeSet = true; + if (explicitMode) + transmitter->setModeSetAndMode(modeSet, mode); + else + transmitter->setModeSet(modeSet); + receiver->setModeSet(modeSet); receptionTimer = 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) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 087703e87f5..3e60982dd55 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: /** @@ -32,11 +35,15 @@ 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; 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; @@ -48,7 +55,13 @@ 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 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); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned index 86b85675aa6..56e5e2c1a9a 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/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h index d71d16deee1..f195387a582 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -38,6 +38,7 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase, public IIeee80211Rec 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..bcbe608b091 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -72,19 +72,42 @@ const Ieee80211Channel *Ieee80211Transmitter::computeTransmissionChannel(const P return transmissionChannel; } +const IIeee80211Mode *Ieee80211Transmitter::computeModeForModeSet(const Ieee80211ModeSet *modeSet) const +{ + if (this->modeSet == modeSet) + return mode; + if (modeSet == nullptr) + return nullptr; + if (mode != nullptr && !modeSet->containsMode(mode)) { + auto 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()); + return newMode; + } + return mode; +} + void Ieee80211Transmitter::setModeSet(const Ieee80211ModeSet *modeSet) { if (this->modeSet != modeSet) { + auto newMode = computeModeForModeSet(modeSet); 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 354a42153bd..ce0e61090e5 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -39,7 +39,15 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase, public IIeee80 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; } + // Resolves a catalog-only change without mutation; throws if no exact tuple exists. + virtual const IIeee80211Mode *computeModeForModeSet(const Ieee80211ModeSet *modeSet) const; 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/Ieee80211AlternativeRadioStartup_1.test b/tests/module/Ieee80211AlternativeRadioStartup_1.test new file mode 100644 index 00000000000..278e6819616 --- /dev/null +++ b/tests/module/Ieee80211AlternativeRadioStartup_1.test @@ -0,0 +1,84 @@ +%description: +Ieee80211Interface initializes with the actual GenericRadio and Ieee80211OfdmRadio +replaceable types, neither of which declares modeSetCoordinatorModule. Compound +parameter patterns must not impose that parameter on those radio implementations. + +%file: AlternativeRadioStartup.cc +#include "inet/common/InitStages.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +using namespace inet; +class AlternativeRadioStartup : public cSimpleModule +{ + protected: + virtual void initialize() override { scheduleAt(SIMTIME_ZERO, new cMessage("check startup")); } + virtual void handleMessage(cMessage *message) override + { + delete message; + auto radio = getModuleByPath("^.host.wlan[0].radio"); + ASSERT(dynamic_cast(radio) != nullptr); + ASSERT(!radio->hasPar("modeSetCoordinatorModule")); + ASSERT(std::string(radio->getModuleType()->getName()) == par("radioType").stdstringValue()); + std::cout << "Alternative radio initialized: " << par("radioType").stdstringValue() << "\n"; + endSimulation(); + } +}; +Define_Module(AlternativeRadioStartup); + +%file: test.ned +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.common.contract.packetlevel.IRadioMedium; +simple AlternativeRadioStartup +{ + parameters: + @class(::AlternativeRadioStartup); + string radioType; +} +network AlternativeRadioNetwork +{ + submodules: + radioMedium: <> like IRadioMedium; + host: AdhocHost; + test: AlternativeRadioStartup; +} + +%inifile: omnetpp.ini +[General] +network = AlternativeRadioNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +sim-time-limit = 1ms +record-vector-results = false +record-scalar-results = false +*.test.radioType = ${radio="GenericRadio","Ieee80211OfdmRadio"} +*.host.wlan[*].radio.typename = ${radio} +*.radioMedium.typename = ${radio} == "GenericRadio" ? "UnitDiskRadioMedium" : "Ieee80211DimensionalRadioMedium" +*.host.mobility.initFromDisplayString = false +*.host.mobility.initialX = 10m +*.host.mobility.initialY = 10m +*.host.mobility.initialZ = 0m +*.host.wlan[*].typename = "Ieee80211Interface" +*.host.wlan[*].opMode = "g(mixed)" +*.host.wlan[*].bitrate = 24Mbps +*.host.wlan[*].radio.signalAnalogRepresentation = ${radio} == "GenericRadio" ? "unitDisk" : "dimensional" +*.host.wlan[*].radio.transmitter.bitrate = 2Mbps +*.host.wlan[*].radio.transmitter.preambleDuration = 0s +*.host.wlan[*].radio.transmitter.headerLength = 96b +*.host.wlan[*].radio.transmitter.analogModel.communicationRange = 100m +*.host.wlan[*].radio.transmitter.analogModel.interferenceRange = 0m +*.host.wlan[*].radio.transmitter.analogModel.detectionRange = 0m +*.host.wlan[*].radio.receiver.ignoreInterference = true +*.host.wlan[*].radio.centerFrequency = 2.4GHz +*.host.wlan[*].radio.bandwidth = 20MHz +*.host.wlan[*].radio.transmitter.power = 0.1mW +*.host.wlan[*].radio.receiver.sensitivity = -100dBm +*.host.wlan[*].radio.receiver.snirThreshold = 4dB +*.host.wlan[*].radio.receiver.energyDetection = -90dBm +*.host.wlan[*].radio.receiver.channelSpacing = 20MHz +**.levelOfDetail = "symbol" +**.isCompliant = true + +%contains: stdout +Alternative radio initialized: GenericRadio + +%contains: stdout +Alternative radio initialized: Ieee80211OfdmRadio diff --git a/tests/module/Ieee80211ContentionModeSet_1.test b/tests/module/Ieee80211ContentionModeSet_1.test new file mode 100644 index 00000000000..33f605972f5 --- /dev/null +++ b/tests/module/Ieee80211ContentionModeSet_1.test @@ -0,0 +1,210 @@ +%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/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" + +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: + 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; + 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()->getSubmodule("mac")); + 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; + if (contention == nullptr) { + phase = par("phase").stdstringValue(); + auto nic = getParentModule()->getSubmodule("ap")->getSubmodule("wlan", 0); + radio = check_and_cast(nic->getSubmodule("radio")); + 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); + 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(currentCw() == 63); + ASSERT(modeNotifications == (phase == "unchanged" ? 0 : 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)); + 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); + 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(); + } +}; +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 = -1 +**.cwMax = -1 + +%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 diff --git a/tests/module/Ieee80211MgmtApChannelChange_1.test b/tests/module/Ieee80211MgmtApChannelChange_1.test index 3a0969059bf..cd306703a90 100644 --- a/tests/module/Ieee80211MgmtApChannelChange_1.test +++ b/tests/module/Ieee80211MgmtApChannelChange_1.test @@ -240,6 +240,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 capabilities = peerState->negotiatedCapabilities; + radio->setModeSetAndMode(modeSet, modeSet->getMode(Mbps(24))); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 0); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(20)); + peerState = mib->findPeerHtState(staAddress); + ASSERT(peerState->negotiatedCapabilities == capabilities); + ASSERT(mib->getHtOperation().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); @@ -341,6 +354,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. @@ -429,7 +457,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 @@ -462,3 +490,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. diff --git a/tests/module/Ieee80211MgmtApUnavailableChannel_1.test b/tests/module/Ieee80211MgmtApUnavailableChannel_1.test index 746235f952e..15349362752 100644 --- a/tests/module/Ieee80211MgmtApUnavailableChannel_1.test +++ b/tests/module/Ieee80211MgmtApUnavailableChannel_1.test @@ -1,8 +1,9 @@ %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 when the transmitter is also unconfigured, so initialization reports the -missing MIB-owned channel instead of serializing an invalid value. +initial channel signal when the transmitter is also unconfigured, 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 new file mode 100644 index 00000000000..b1059a8e9ed --- /dev/null +++ b/tests/module/Ieee80211ModeSetFailure_1.test @@ -0,0 +1,131 @@ +%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. 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; +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(); + auto failure = par("failure").stdstringValue(); + // A participant exception must prevent completion publication. + ASSERT(failure != "dcf" && failure != "hcf"); + if (failure == "listening") + return; // Application completed; the later listening observer fails. + if (par("failure").stdstringValue() == "detach") + check_and_cast(source)->unregisterModeSetConsumer(check_and_cast(source)->getSubmodule("mac")); + if (par("failure").stdstringValue() == "reentrant") + check_and_cast(check_and_cast(source)->getParentModule()->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 + { + Enter_Method_Silent(); + ASSERT(par("failure").stdstringValue() == "listening"); + 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(); + radio->getParentModule()->subscribe(modesetChangedSignal, this); + if (failure == "listening" || failure == "dcf" || failure == "hcf") + 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 + 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","band","detach"} +*.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", "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) == 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("has no standards channel-number mapping" 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..1b09af9a1e3 --- /dev/null +++ b/tests/module/Ieee80211ModeSetRegistration_1.test @@ -0,0 +1,262 @@ +%description: +Explicit mode-set registration is independent of observer subscriptions and registration +order. Initialization queries the catalog; the MAC publishes 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/mac/contract/IIeee80211ModeSetProvider.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/Ieee80211RadioChannelChangedDetails.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/generic/GenericTransmitter.h" +#include "inet/physicallayer/wireless/generic/GenericReceiver.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211TransmitterCapabilities.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211ReceiverCapabilities.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class CapabilityTransmitter : public GenericTransmitter, public IIeee80211TransmitterCapabilities +{ + public: + bool isHtChannelWidthSupported(Hz width) const override { return width == MHz(20) || width == MHz(40); } +}; +Define_Module(CapabilityTransmitter); +class CapabilityReceiver : public GenericReceiver, public IIeee80211ReceiverCapabilities +{ + public: + bool isHtChannelWidthSupported(Hz width) const override { return width == MHz(20) || width == MHz(40); } + bool isHtShortGuardIntervalSupported(Hz) const override { return false; } +}; +Define_Module(CapabilityReceiver); + +// 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) { + Ieee80211RadioChannelChangedDetails details(channel.getBand()); + emit(cComponent::registerSignal("radioChannelChanged"), 10L, &details); + } + } +}; +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"))->isLocalHtCapable() == 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)->getSubmodule("mac")); + 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); + } + else if (stage == INITSTAGE_LINK_LAYER) + modeSet = check_and_cast(getContainingNicModule(this)->getSubmodule("mac"))->getConfiguredModeSet(); + } +}; +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)->getSubmodule("mac") || source == nic(1)->getSubmodule("mac")); + auto module = check_and_cast(source)->getParentModule(); + 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->getLocalHtCapabilities().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 == 0 && observer.notifications == 0); + auto first = nic(0); + auto second = nic(1); + auto radio = check_and_cast(first->getSubmodule("radio")); + auto coordinator = check_and_cast(first->getSubmodule("mac")); + auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); + auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + ASSERT(notifications == 1 && observer.notifications == 1); + ASSERT(probe(first, 0)->applications == 1 && probe(first, 1)->applications == 1); + ASSERT(probe(second, 0)->applications == 0 && probe(second, 1)->applications == 0); + getParentModule()->unsubscribe(modesetChangedSignal, this); + getParentModule()->unsubscribe(modesetChangedSignal, &observer); + radio->setModeSet(ht); + ASSERT(probe(first, 0)->applications == 2 && probe(first, 1)->applications == 2); + ASSERT(notifications == 1 && observer.notifications == 1); + 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 == 3); + ASSERT(probe(second, 0)->applications == 0); + 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.generic.GenericTransmitter; +import inet.physicallayer.wireless.generic.GenericReceiver; +import inet.physicallayer.wireless.ieee80211.contract.packetlevel.IIeee80211Radio; +module CapabilityRadio extends GenericUnitDiskRadio like IIeee80211Radio +{ + parameters: + @class(::CapabilityRadio); + transmitter.typename = "CapabilityTransmitter"; + receiver.typename = "CapabilityReceiver"; +} +module CapabilityTransmitter extends GenericTransmitter { parameters: @class(::CapabilityTransmitter); } +module CapabilityReceiver extends GenericReceiver { parameters: @class(::CapabilityReceiver); } +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/Ieee80211ModeSetRetry_1.test b/tests/module/Ieee80211ModeSetRetry_1.test new file mode 100644 index 00000000000..f34eee71935 --- /dev/null +++ b/tests/module/Ieee80211ModeSetRetry_1.test @@ -0,0 +1,307 @@ +%description: +DCF and HCF recompute retained HT mode requests after a coordinated legacy +transition. Drop exactly one data frame or RTS at the recipient, retain the real +queued MPDU through the timeout, then transmit and acknowledge it in g(mixed). +Peer capabilities are seeded; this tests MAC reconfiguration, not association. + +%file: ModeSetRetry.cc +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" +#include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class RetryProbeMac : public Ieee80211Mac +{ + public: + bool blocked = false; + bool switched = false; + int htDataAttempts = 0; + int legacyDataAttempts = 0; + int rtsAttempts = 0; + int delivered = 0; + long dataTreeId = -1; + int sequence = -1; + InProgressFrames *retainedQueue = nullptr; + Packet *retainedFrame = nullptr; + + virtual void sendDownFrame(Packet *packet) override + { + if (par("sender")) { + auto header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + auto mode = packet->getTag()->getMode(); + if (!switched) { + ASSERT(mode->getHtMcsIndex() >= 0); + ASSERT(!data->getRetry()); + htDataAttempts++; + } + else { + ASSERT(Ieee80211ModeSet::getModeSet("g(mixed)")->containsMode(mode)); + ASSERT(mode->getHtMcsIndex() < 0); + ASSERT(packet->getTreeId() == dataTreeId); + ASSERT(retainedQueue->getLength() == 1 && retainedQueue->getFrames(0) == retainedFrame); + ASSERT(retainedFrame->getTag()->getMode() == mode); + ASSERT(data->getSequenceNumber().get() == sequence); + if (!par("protection")) + ASSERT(data->getRetry()); + legacyDataAttempts++; + } + } + else if (header->getType() == ST_RTS) + rtsAttempts++; + } + Ieee80211Mac::sendDownFrame(packet); + } + + virtual void sendUpFrame(Packet *packet) override + { + // End the fixture at the MAC service boundary after normal recipient + // processing and ACK scheduling; no synthetic LLC payload is decoded. + delivered++; + delete packet; + } + + protected: + virtual void handleLowerPacket(Packet *packet) override + { + auto header = packet->peekAtFront(); + bool target = par("protection") ? header->getType() == ST_RTS : + dynamicPtrCast(header) != nullptr; + if (!par("sender") && !blocked && target) { + blocked = true; + std::cout << "Controlled first reception loss\n"; + delete packet; + } + else + Ieee80211Mac::handleLowerPacket(packet); + } +}; +Define_Module(RetryProbeMac); + +class ModeSetRetryTest : public cSimpleModule, public cListener +{ + using cListener::finish; + RetryProbeMac *sender = nullptr; + RetryProbeMac *receiver = nullptr; + InProgressFrames *queue = nullptr; + Packet *retained = nullptr; + opp_component_ptr coordination; + cMessage *action = nullptr; + int phase = 0; + int sequences = 0; + int acknowledgments = 0; + + int retryCount() + { + auto header = retained->peekAtFront(); + if (par("qos")) + return check_and_cast(queue->getParentModule()->getSubmodule("recoveryProcedure"))->getRetryCount(retained, header); + return check_and_cast(coordination->getSubmodule("recoveryProcedure"))->getRetryCount(retained, header); + } + + Ieee80211Radio *radio(RetryProbeMac *mac) + { + return check_and_cast(mac->getParentModule()->getSubmodule("radio")); + } + + protected: + virtual void initialize() override + { + sender = check_and_cast(getModuleByPath("^.host[0].wlan[0].mac")); + receiver = check_and_cast(getModuleByPath("^.host[1].wlan[0].mac")); + coordination = sender->getSubmodule(par("qos") ? "hcf" : "dcf"); + coordination->subscribe(IFrameSequenceHandler::frameSequenceFinishedSignal, this); + coordination->subscribe(packetReceivedFromPeerSignal, this); + action = new cMessage("fixture action"); + scheduleAt(SimTime(1, SIMTIME_MS), action); + } + + virtual void receiveSignal(cComponent *, simsignal_t signal, cObject *value, cObject *) override + { + Enter_Method_Silent(); + if (signal == packetReceivedFromPeerSignal) { + auto packet = check_and_cast(value); + if (packet->peekAtFront()->getType() == ST_ACK) + acknowledgments++; + return; + } + auto context = check_and_cast(value); + queue = context->getInProgressFrames(); + sequences++; + ASSERT(sequences <= 2); + if (phase == 1) { + ASSERT(receiver->blocked); + ASSERT(queue->getLength() == 1); + retained = queue->getFrames(0); + ASSERT(retained->getTag()->getMode()->getHtMcsIndex() >= 0); + ASSERT(sender->dataTreeId == retained->getTreeId()); + sender->retainedQueue = queue; + sender->retainedFrame = retained; + if (!par("protection")) + ASSERT(retryCount() == 1); + sender->sequence = retained->peekAtFront()->getSequenceNumber().get(); + ASSERT(sender->htDataAttempts == (par("protection") ? 0 : 1)); + ASSERT(sender->rtsAttempts == (par("protection") ? 1 : 0)); + phase = 2; + } + else { + ASSERT(phase == 3); + ASSERT(queue->getLength() == 0); // The real ACK handler removed it. + retained = nullptr; + sender->retainedFrame = nullptr; + phase = 4; + } + scheduleAt(simTime(), action); // Run after frame-sequence cleanup. + } + + virtual void handleMessage(cMessage *) override + { + if (phase == 0) { + auto senderMib = check_and_cast(sender->getParentModule()->getSubmodule("mib")); + auto receiverMib = check_and_cast(receiver->getParentModule()->getSubmodule("mib")); + // Install the explicit test relationship; ad hoc initialization itself + // intentionally does not invent an accepted HT operation or peer. + for (auto mac : {sender, receiver}) { + auto mib = check_and_cast(mac->getParentModule()->getSubmodule("mib")); + Ieee80211HtOperation operation; + operation.primaryChannel = 6; + operation.operatingChannelWidth = MHz(20); + mib->commitBss("retry-test", sender->getAddress(), radio(mac)->getChannel()->getBand(), 6, &operation); + } + for (auto mac : {sender, receiver}) + radio(mac)->setMode(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")->getMode(Mbps(24))); + senderMib->setPeerHtCapabilities(receiver->getAddress(), receiverMib->getLocalHtCapabilities()); + receiverMib->setPeerHtCapabilities(sender->getAddress(), senderMib->getLocalHtCapabilities()); + auto packet = new Packet("retained-data"); + auto header = makeShared(); + header->setType(par("qos") ? ST_DATA_WITH_QOS : ST_DATA); + header->setChunkLength(DATAFRAME_HEADER_MINLENGTH + (par("qos") ? QOSCONTROL_PART_LENGTH : b(0))); + header->setTid(0); + header->setReceiverAddress(receiver->getAddress()); + header->setTransmitterAddress(sender->getAddress()); + header->setAddress3(receiver->getAddress()); + packet->insertAtBack(header); + packet->insertAtBack(makeShared(B(100))); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(trailer); + sender->dataTreeId = packet->getTreeId(); + phase = 1; + sender->processUpperFrame(packet, header); + } + else if (phase == 2) { + ASSERT(queue->getLength() == 1 && queue->getFrames(0) == retained); + auto oldMode = retained->getTag()->getMode(); + for (auto mac : {sender, receiver}) { + ASSERT(radio(mac)->getTransmissionState() != IRadio::TRANSMISSION_STATE_TRANSMITTING); + ASSERT(radio(mac)->getReceptionState() == IRadio::RECEPTION_STATE_IDLE); + radio(mac)->setModeSet(Ieee80211ModeSet::getModeSet("g(mixed)")); + } + ASSERT(retained->getTag()->getMode() == oldMode); + if (!par("protection")) + ASSERT(retryCount() == 1); + sender->switched = true; + phase = 3; + std::cout << "Retained HT request survives transition until reselection\n"; + } + else { + ASSERT(phase == 4 && sequences == 2); + ASSERT(receiver->delivered == 1 && acknowledgments == 1); + ASSERT(sender->legacyDataAttempts == 1); + ASSERT(sender->rtsAttempts == (par("protection") ? 2 : 0)); + std::cout << "Retained frame transmitted and acknowledged in new mode set: qos=" + << par("qos").boolValue() << " protection=" << par("protection").boolValue() << "\n"; + endSimulation(); + } + } + + virtual void finish() override { ASSERT(phase == 4); } + + public: + virtual ~ModeSetRetryTest() + { + if (coordination) { + coordination->unsubscribe(IFrameSequenceHandler::frameSequenceFinishedSignal, this); + coordination->unsubscribe(packetReceivedFromPeerSignal, this); + } + cancelAndDelete(action); + } +}; +Define_Module(ModeSetRetryTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module RetryProbeMac extends Ieee80211Mac +{ + parameters: + @class(::RetryProbeMac); + bool sender; + bool protection; +} +simple ModeSetRetryTest +{ + parameters: + @class(::ModeSetRetryTest); + bool qos; + bool protection; +} +network RetryNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host[2]: WirelessHost; + test: ModeSetRetryTest; +} + +%inifile: omnetpp.ini +[General] +network = RetryNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 20ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.test.qos = ${qos=false,true} +*.test.protection = ${protection=false,true} +**.mobility.initFromDisplayString = false +*.host[0].mobility.initialX = 10m +*.host[1].mobility.initialX = 11m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = -1bps +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].agent.typename = "" +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 6 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.antenna.numAntennas = 1 +**.wlan[*].mac.typename = "RetryProbeMac" +*.host[0].wlan[*].mac.sender = true +*.host[1].wlan[*].mac.sender = false +**.mac.protection = ${protection} +**.mac.qosStation = ${qos} +**.mac.*.rateControl.typename = "AarfRateControl" +**.mac.*.rtsPolicy.rtsThreshold = ${protection} ? 0B : 10000B + +%file: check.py +from pathlib import Path +text = Path("test.out").read_text() +for qos in (0, 1): + for protection in (0, 1): + assert text.count(f"Retained frame transmitted and acknowledged in new mode set: qos={qos} protection={protection}") == 1 +assert text.count("Controlled first reception loss") == 4 +assert text.count("Retained HT request survives transition until reselection") == 4 + +%postrun-command: python3 check.py diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test new file mode 100644 index 00000000000..65f100de451 --- /dev/null +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -0,0 +1,314 @@ +%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/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" +#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 *module) + { + std::vector result; + 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; + } + + 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(nic)) + ASSERT(participant->getModeSet() == expected); + auto mib = check_and_cast(nic->getSubmodule("mib")); + ASSERT(mib->isLocalHtCapable() == expected->isHtOperationSupported()); + ASSERT(mib->requirePrimaryChannel() == 6); + ASSERT((mib->findPeerHtState(peer) != nullptr) == peerExpected); + for (int i = 0; i < 77; i++) { + ASSERT(mib->getLocalHtCapabilities().rxMcsSupported[i] == (expected->isHtOperationSupported() && i < 8)); + if (mib->hasHtOperation()) + ASSERT(mib->getHtOperation().basicMcsSupported[i] == (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"); + // 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")); + 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 nic = check_and_cast(source)->getParentModule(); + ASSERT(nic->getSubmodule("mac") != nullptr); + auto target = check_and_cast(value); + observedModeSet = target; + // Publication must follow *all* internal updates, irrespective of where + // this observer appears in the subscriber list. + for (auto participant : participants(nic)) + ASSERT(participant->getModeSet() == target); + ASSERT(check_and_cast(nic->getSubmodule("mib"))->isLocalHtCapable() == 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; + nic->subscribe(modesetChangedSignal, &cachingObserver); + nic->subscribe(modesetChangedSignal, this); + mib->setPeerHtCapabilities(peer, mib->getLocalHtCapabilities()); + check(nic, ht, true); + auto originalCapabilities = mib->findPeerCapabilities(peer)->negotiatedCapabilities; + radio->setModeSet(ht); + ASSERT(mib->findPeerCapabilities(peer)->negotiatedCapabilities == originalCapabilities); + ASSERT(notifications == 0 && cachingObserver.notifications == 0); + 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(mib->findPeerCapabilities(peer) != nullptr); + ASSERT(mib->findPeerCapabilities(peer)->negotiatedCapabilities != originalCapabilities); + 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, true); + ASSERT(ht->containsMode(arf->getRate(peer))); + ASSERT(ht->containsMode(onoe->getRate(peer))); + mib->setPeerHtCapabilities(peer, mib->getLocalHtCapabilities()); + check(nic, ht, true); + ASSERT(notifications == 2); + ASSERT(medium->listeningNotifications == oldListening + 2); + ASSERT(medium->lastListeningSource == radio); + ASSERT(cachingObserver.modeSet == ht); + ASSERT(observedModeSet == ht); + // A failed catalog-only preflight must leave both PHY endpoints, MAC + // consumers and observations untouched, and allow an explicit retry. + auto incompatibleMode = ht->getMode(Mbps(135), MHz(40), 1, 800e-9); + radio->setMode(incompatibleMode); + auto retainedCapabilities = mib->findPeerCapabilities(peer)->negotiatedCapabilities; + int beforeRejectedListening = medium->listeningNotifications; + for (int attempt = 0; attempt < 2; attempt++) { + bool rejected = false; + try { radio->setModeSet(legacy); } + catch (const cRuntimeError& error) { + rejected = std::string(error.what()).find("Cannot map current mode") != std::string::npos; + } + ASSERT(rejected); + check(nic, ht, true); + ASSERT(check_and_cast(radio->getTransmitter())->getMode() == incompatibleMode); + ASSERT(mib->findPeerCapabilities(peer)->negotiatedCapabilities == retainedCapabilities); + ASSERT(notifications == 2 && cachingObserver.notifications == 2); + ASSERT(medium->listeningNotifications == beforeRejectedListening); + } + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + check(nic, legacy, false); + ASSERT(notifications == 3 && cachingObserver.notifications == 3); + ASSERT(medium->listeningNotifications == beforeRejectedListening + 1); + ASSERT(cachingObserver.modeSet == legacy && observedModeSet == legacy); + nic->unsubscribe(modesetChangedSignal, this); + nic->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/module/Ieee80211TxopModeSet_1.test b/tests/module/Ieee80211TxopModeSet_1.test new file mode 100644 index 00000000000..39ea8d97409 --- /dev/null +++ b/tests/module/Ieee80211TxopModeSet_1.test @@ -0,0 +1,98 @@ +%description: +A runtime radio mode-set transition updates default TXOP limits for the next +TXOP, preserving an active TXOP and explicit overrides. Exercise every AC, +both b/g transition directions, and default, zero and positive overrides. + +%file: TxopModeSet.cc +#include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class TxopModeSetTest : public cSimpleModule +{ + protected: + virtual void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("check")); } + virtual void handleMessage(cMessage *message) override + { + delete message; + auto nic = getParentModule()->getSubmodule("ap")->getSubmodule("wlan", 0); + auto radio = check_and_cast(nic->getSubmodule("radio")); + auto edca = nic->getModuleByPath(".mac.hcf.edca"); + auto bModes = Ieee80211ModeSet::getModeSet("b"); + auto gModes = Ieee80211ModeSet::getModeSet("g(mixed)"); + // Existing modeled defaults (IEEE Std 802.11-2024, Table 9-194; + // TxopProcedure documents the retained legacy table values). + const int bLimitsUs[] = {0, 0, 6016, 3264}; + const int gLimitsUs[] = {0, 0, 3008, 1504}; + for (int index = 0; index < 4; index++) { + auto txop = check_and_cast(edca->getSubmodule("edcaf", index)->getSubmodule("txopProcedure")); + auto ac = static_cast(index); + simtime_t overrideLimit = txop->par("txopLimit"); + auto bLimit = overrideLimit == -1 ? SimTime(bLimitsUs[index], SIMTIME_US) : overrideLimit; + auto gLimit = overrideLimit == -1 ? SimTime(gLimitsUs[index], SIMTIME_US) : overrideLimit; + txop->startTxop(ac); + ASSERT(txop->getLimit() == bLimit); + radio->setModeSetAndMode(gModes, gModes->getMode(Mbps(24))); + ASSERT(txop->getModeSet() == gModes); + ASSERT(txop->getLimit() == bLimit); + ASSERT(txop->getRemaining() == bLimit); + txop->endTxop(); + txop->startTxop(ac); + ASSERT(txop->getLimit() == gLimit); + ASSERT(txop->getRemaining() == gLimit); + txop->endTxop(); + radio->setModeSetAndMode(bModes, bModes->getMode(Mbps(11))); + txop->startTxop(ac); + ASSERT(txop->getLimit() == bLimit); + ASSERT(txop->getRemaining() == bLimit); + txop->endTxop(); + } + std::cout << "Verified TXOP limits across mode transitions" << std::endl; + endSimulation(); + } +}; +Define_Module(TxopModeSetTest); + +%file: test.ned +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple TxopModeSetTest +{ + parameters: + @class(::TxopModeSetTest); +} +network TxopModeSetNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + test: TxopModeSetTest; +} + +%inifile: omnetpp.ini +[General] +network = TxopModeSetNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +sim-time-limit = 2ms +record-scalar-results = false +record-vector-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.wlan[*].opMode = "b" +**.wlan[*].bitrate = -1bps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 6 +**.wlan[*].mac.qosStation = true +**.txopProcedure.txopLimit = ${-1s,0s,1ms} +*.ap.wlan[0].mgmt.typename = "Ieee80211MgmtApSimplified" + +%file: check_results.py +from pathlib import Path +assert Path("test.out").read_text().count("Verified TXOP limits across mode transitions") == 3 + +%postrun-command: python3 check_results.py diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 7f66eb5c566..8c5cec83e78 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -37,6 +37,97 @@ class AbsentGiMode : public Ieee80211OfdmMode virtual const Ieee80211OfdmDataMode *getDataMode() const override { return &data; } }; +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) { + rs->applyModeSet(ms); + } +}; + +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) { + rs->applyModeSet(ms); + } +}; + +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) + { + applyModeSet(modeSet); + } + + const ieee80211::Ieee80211SupportedRatesElement& getSupportedRates() const { return supportedRates; } +}; + %activity: // Reject unsupported VHT greenfield before any mixed-format cache request. bool rejectedVhtGreenfieldBeforeMixed = false; @@ -53,6 +144,26 @@ 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; @@ -170,6 +281,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); @@ -204,6 +329,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); @@ -289,6 +436,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 ae9b366e4a26ee0103619a780794711f63cfec17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:54:28 +0200 Subject: [PATCH 15/21] ieee80211: fix: preserve reception state during radio reconfiguration Reapplying an unchanged channel or changing only the transmit mode must not interrupt a compatible incoming frame. Evaluate the actual incoming mode against listening created from the new receiver configuration; preserve compatible reception without drawing another error decision. Abort incompatible reception through the normal cleanup path before configuration notifications. Clearing only the reception pointer left the radio reporting RECEIVING until a later signal boundary. Retain arrival timers for cleanup and give receiver and transmitter separate channel objects. Cover whole and separate reception parts, unchanged settings, transmit-only changes, incompatible configurations, cleanup and subsequent reception. Plan: plan/done/ht-gi-devin-comment-closure.md Change: src.ieee80211 | behavior.change.fix | test whatsnew migration --- WHATSNEW | 5 + doc/src/migration-guide/index.rst | 10 + .../ieee80211/packetlevel/Ieee80211Radio.cc | 28 +- .../ieee80211/packetlevel/Ieee80211Radio.h | 1 + .../Ieee80211ReceptionReconfiguration_1.test | 254 ++++++++++++++++++ 5 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 tests/module/Ieee80211ReceptionReconfiguration_1.test diff --git a/WHATSNEW b/WHATSNEW index 0d3e4bb460d..1ebf6258759 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -385,6 +385,11 @@ Notable backward compatible changes are the following: migration guide. Default TXOP limits are resolved for each new TXOP after a mode-set change; active TXOPs and configured overrides retain their limits. + Radio reconfiguration preserves ongoing receptions when the incoming mode + and receiver listening configuration remain compatible. Transmit-only mode + changes and reapplying unchanged settings no longer interrupt reception. + Incompatible changes abort through the normal reception cleanup path. + 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 diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index a07f1dcaa82..e0c05277abe 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -105,6 +105,16 @@ answers; :cpp:`Arp` shows how. An implementation that resolves addresses without packets has nobody to ask. It overrides the method with an empty body, as :cpp:`GlobalArp` does, and the client then takes the address. +IEEE 802.11 Radio Reconfiguration +-------------------------------- + +Radio setters no longer implicitly interrupt compatible ongoing receptions. +Changing the transmit mode alone, reapplying unchanged receiver settings, or +changing the mode set while retaining the incoming mode preserves reception. +An incompatible receiver configuration still aborts reception and retains +arrival timers for normal cleanup. Custom callers should not rely on a no-op +setter or a transmit-mode change to cancel reception. + Migrating IEEE 802.11 PHY Modes ------------------------------ diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 7b4211a78a1..ad9748e6094 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -9,12 +9,14 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211RadioChannelChangedDetails.h" #include +#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/common/signal/WirelessSignal.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" @@ -132,7 +134,7 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 else transmitter->setModeSet(modeSet); receiver->setModeSet(modeSet); - receptionTimer = nullptr; + abortIncompatibleReception(); if (modeSetCoordinator != nullptr) modeSetCoordinator->completeModeSetChange(modeSet); else if (modeSet != nullptr) @@ -147,6 +149,23 @@ const Ieee80211Channel *Ieee80211Radio::getChannel() const return check_and_cast(transmitter)->getChannel(); } +void Ieee80211Radio::abortIncompatibleReception() +{ + if (receptionTimer == nullptr) + return; + auto signal = check_and_cast(receptionTimer->getControlInfo()); + auto reception = signal->getReception(); + // Use the new receiver configuration, not the medium's cached listening or + // the local transmit mode. Possibility does not draw another error decision. + std::unique_ptr listening(receiver->createListening(this, + reception->getStartTime(), reception->getEndTime(), + reception->getStartPosition(), reception->getEndPosition())); + auto ieee80211Receiver = check_and_cast(receiver); + if (ieee80211Receiver->getModeSet() == nullptr || + !receiver->computeIsReceptionPossible(listening.get(), reception, (IRadioSignal::SignalPart)receptionTimer->getKind())) + abortReception(receptionTimer); +} + bool Ieee80211Radio::isHtChannelWidthSupported(Hz channelWidth) const { return check_and_cast(transmitter)->isHtChannelWidthSupported(channelWidth) && @@ -158,7 +177,6 @@ void Ieee80211Radio::setMode(const IIeee80211Mode *mode) Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); ieee80211Transmitter->setMode(mode); EV << "Changing radio mode to " << mode << endl; - receptionTimer = nullptr; emit(listeningChangedSignal, 0); } @@ -169,7 +187,7 @@ void Ieee80211Radio::setBand(const IIeee80211Band *band) ieee80211Transmitter->setBand(band); ieee80211Receiver->setBand(band); EV << "Changing radio band to " << band << endl; - receptionTimer = nullptr; + abortIncompatibleReception(); const auto *channel = ieee80211Transmitter->getChannel(); if (channel != nullptr) { Ieee80211RadioChannelChangedDetails details(channel->getBand()); @@ -187,7 +205,7 @@ void Ieee80211Radio::setChannel(const Ieee80211Channel *channel) ieee80211Transmitter->setChannel(channel); ieee80211Receiver->setChannel(new Ieee80211Channel(channel->getBand(), channel->getChannelNumber())); EV << "Changing radio channel to " << channel->getChannelNumber() << endl; - receptionTimer = nullptr; + abortIncompatibleReception(); Ieee80211RadioChannelChangedDetails details(channel->getBand()); emit(radioChannelChangedSignal, channel->getChannelNumber(), &details); emit(listeningChangedSignal, 0); @@ -200,7 +218,7 @@ void Ieee80211Radio::setChannelNumber(int newChannelNumber) ieee80211Transmitter->setChannelNumber(newChannelNumber); ieee80211Receiver->setChannelNumber(newChannelNumber); EV << "Changing radio channel to " << newChannelNumber << ".\n"; - receptionTimer = nullptr; + abortIncompatibleReception(); Ieee80211RadioChannelChangedDetails details(ieee80211Transmitter->getChannel()->getBand()); emit(radioChannelChangedSignal, newChannelNumber, &details); emit(listeningChangedSignal, 0); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 3e60982dd55..3d526363aea 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -43,6 +43,7 @@ class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio virtual void initialize(int stage) override; void changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode); + void abortIncompatibleReception(); virtual void handleUpperCommand(cMessage *message) override; diff --git a/tests/module/Ieee80211ReceptionReconfiguration_1.test b/tests/module/Ieee80211ReceptionReconfiguration_1.test new file mode 100644 index 00000000000..cda32204264 --- /dev/null +++ b/tests/module/Ieee80211ReceptionReconfiguration_1.test @@ -0,0 +1,254 @@ +%description: +Reconfigure during HT and legacy receptions through each IEEE 802.11 radio setter. +Compatible changes retain decoding and deliver the original packet exactly once. +Removing the incoming mode or retuning interrupts decoding, retains arrival timers +until cleanup, and suppresses the original packet. Both paths receive a later packet. +Exercise whole-signal and separate-part reception with seed 0. + +%file: ReceptionReconfiguration.cc +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::physicallayer; + +class ReconfigurationRadio : public Ieee80211Radio +{ + public: + bool hasActiveReception() const { return receptionTimer != nullptr; } + size_t getArrivalTimerCount() const { return allReceptionTimers.size(); } + void reconfigure(int operation) + { + Enter_Method_Silent(); + const auto *legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); + switch (operation) { + case 0: setModeSet(legacy); break; + case 1: setModeSetAndMode(legacy, legacy->getMode(Mbps(6))); break; + case 2: setMode(legacy->getMode(Mbps(6))); break; + case 3: setBand(getChannel()->getBand()); break; + case 4: { + setChannel(new Ieee80211Channel(getChannel()->getBand(), 11)); + break; + } + case 5: setChannelNumber(11); break; + case 6: setModeSet(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); break; + case 7: setMode(legacy->getMode(Mbps(12))); break; + case 8: setChannel(new Ieee80211Channel(getChannel()->getBand(), getChannel()->getChannelNumber())); break; + case 9: setChannelNumber(getChannel()->getChannelNumber()); break; + case 10: setBand(Ieee80211CompliantBands::getBand("5 GHz")); break; + default: throw cRuntimeError("Unknown operation"); + } + } + void prepareNextReception() + { + Enter_Method_Silent(); + // Reusing the radio-owned channel must also preserve distinct PHY owners. + setChannel(getChannel()); + setModeSet(Ieee80211ModeSet::getModeSet("g(mixed)")); + setBand(Ieee80211CompliantBands::getBand("2.4 GHz")); + setChannelNumber(6); + } +}; +Define_Module(ReconfigurationRadio); + +class ReceptionReconfigurationTest : public cSimpleModule, public cListener +{ + int delivered = 0; + int stateNotifications = 0; + int partNotifications = 0; + int configurationNotifications = 0; + bool switching = false; + + ReconfigurationRadio *receiverRadio() const + { + return check_and_cast(getParentModule()->getSubmodule("rx")); + } + bool expectAbort() const + { + int operation = par("operation"); + return operation == 4 || operation == 5 || operation == 10 || + ((operation == 0 || operation == 1) && par("incomingHt").boolValue()); + } + void checkReconfigured() const + { + if (expectAbort()) + checkAborted(); + else { + auto radio = receiverRadio(); + ASSERT(radio->hasActiveReception()); + ASSERT(radio->getReceptionState() == IRadio::RECEPTION_STATE_RECEIVING); + ASSERT(radio->getReceivedSignalPart() != IRadioSignal::SIGNAL_PART_NONE); + ASSERT(radio->getArrivalTimerCount() == 1); + } + } + void checkAborted() const + { + auto radio = receiverRadio(); + ASSERT(!radio->hasActiveReception()); + ASSERT(radio->getReceptionState() != IRadio::RECEPTION_STATE_RECEIVING); + ASSERT(radio->getReceivedSignalPart() == IRadioSignal::SIGNAL_PART_NONE); + ASSERT(radio->getArrivalTimerCount() == 1); + } + void transmit(bool first) + { + auto packet = new Packet(first ? "original" : "subsequent"); + packet->insertAtBack(makeShared(B(1500))); + bool ht = first && par("incomingHt").boolValue(); + const auto *catalog = Ieee80211ModeSet::getModeSet(ht ? "n(mixed-2.4Ghz)" : "g(mixed)"); + const auto *mode = ht ? catalog->getMode(Mbps(6.5), MHz(20), 1, 800e-9) : catalog->getMode(Mbps(12)); + packet->addTag()->setMode(mode); + send(packet, "out"); + } + protected: + virtual void initialize() override + { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("first", 0)); + scheduleAt(SimTime(1100, SIMTIME_US), new cMessage("change", 1)); + scheduleAt(SimTime(4, SIMTIME_MS), new cMessage("second", 2)); + scheduleAt(SimTime(7, SIMTIME_MS), new cMessage("check", 3)); + } + virtual void receiveSignal(cComponent *, simsignal_t signal, intval_t, cObject *) override + { + if (!switching) + return; + if (signal == IRadio::receptionStateChangedSignal) + stateNotifications++; + else if (signal == IRadio::receivedSignalPartChangedSignal) + partNotifications++; + else { + checkReconfigured(); + configurationNotifications++; + } + } + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override + { + if (switching) { + checkReconfigured(); + configurationNotifications++; + } + } + virtual void handleMessage(cMessage *message) override + { + if (!message->isSelfMessage()) { + auto packet = check_and_cast(message); + if (delivered == 0 && !expectAbort()) + ASSERT(!strcmp(packet->getName(), "original")); + else + ASSERT(!strcmp(packet->getName(), "subsequent")); + ASSERT(!packet->hasBitError()); + delivered++; + delete packet; + return; + } + int phase = message->getKind(); + delete message; + auto radio = receiverRadio(); + if (phase == 0) { + check_and_cast(getParentModule()->getSubmodule("tx"))->setRadioMode(IRadio::RADIO_MODE_TRANSMITTER); + radio->setRadioMode(IRadio::RADIO_MODE_RECEIVER); + for (auto signal : {IRadio::receptionStateChangedSignal, IRadio::receivedSignalPartChangedSignal, + IRadio::listeningChangedSignal, Ieee80211Radio::radioChannelChangedSignal, modesetChangedSignal}) + radio->subscribe(signal, this); + transmit(true); + } + else if (phase == 1) { + ASSERT(radio->hasActiveReception()); + ASSERT(radio->getReceptionState() == IRadio::RECEPTION_STATE_RECEIVING); + ASSERT(radio->getReceivedSignalPart() == (radio->par("separateReceptionParts").boolValue() ? + IRadioSignal::SIGNAL_PART_DATA : IRadioSignal::SIGNAL_PART_WHOLE)); + switching = true; + radio->reconfigure(par("operation")); + switching = false; + checkReconfigured(); + ASSERT(stateNotifications == (expectAbort() ? 1 : 0)); + ASSERT(partNotifications == (expectAbort() ? 1 : 0)); + ASSERT(configurationNotifications >= 1); + } + else if (phase == 2) { + ASSERT(radio->getArrivalTimerCount() == 0); + ASSERT(delivered == (expectAbort() ? 0 : 1)); + radio->prepareNextReception(); + transmit(false); + } + else { + ASSERT(delivered == (expectAbort() ? 1 : 2)); + ASSERT(!radio->hasActiveReception()); + ASSERT(radio->getArrivalTimerCount() == 0); + ASSERT(radio->getReceptionState() == IRadio::RECEPTION_STATE_IDLE); + for (auto signal : {IRadio::receptionStateChangedSignal, IRadio::receivedSignalPartChangedSignal, + IRadio::listeningChangedSignal, Ieee80211Radio::radioChannelChangedSignal, modesetChangedSignal}) + radio->unsubscribe(signal, this); + std::cout << "Reception compatibility, timer cleanup and subsequent delivery verified.\n"; + endSimulation(); + } + } +}; +Define_Module(ReceptionReconfigurationTest); + +%file: test.ned +import inet.common.SimpleModule; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module ReconfigurationRadio extends Ieee80211ScalarRadio +{ + parameters: + @class(::ReconfigurationRadio); +} + +simple ReceptionReconfigurationTest extends SimpleModule +{ + parameters: + int operation; + bool incomingHt; + @class(::ReceptionReconfigurationTest); + gates: + input in; + output out; +} + +network ReceptionNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + tx: Ieee80211ScalarRadio; + rx: ReconfigurationRadio; + test: ReceptionReconfigurationTest; + connections allowunconnected: + test.out --> tx.upperLayerIn; + rx.upperLayerOut --> test.in; +} + +%inifile: omnetpp.ini +[General] +network = ReceptionNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 8ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.test.operation = ${operation=0,1,2,3,4,5,6,7,8,9,10} +*.test.incomingHt = ${incomingHt=false,true} +*.rx.separateReceptionParts = ${parts=false,true} +*.tx.separateTransmissionParts = ${parts} +*.tx.initialRadioMode = "off" +*.rx.initialRadioMode = "off" +*.radioMedium.listeningFilter = true +*.tx.opMode = "n(mixed-2.4Ghz)" +*.rx.opMode = "n(mixed-2.4Ghz)" +**.channelNumber = 6 +**.bandName = "2.4 GHz" +**.transmitter.bitrate = 6Mbps +**.transmitter.power = 100mW +**.antenna.mobilityModule = "" +**.antenna.mobility.typename = "StationaryMobility" +**.antenna.mobility.initFromDisplayString = false +**.antenna.mobility.initialX = 10m +**.antenna.mobility.initialY = 10m +**.antenna.mobility.initialZ = 0m + +%contains: stdout +Reception compatibility, timer cleanup and subsequent delivery verified. From 3e7dca8e67927ed165eed0737e04fda2588ba14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:55:23 +0200 Subject: [PATCH 16/21] ieee80211: fix: 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 frames. Cover zero, one, 32 and 33 octets and truncated payloads. Change: src.ieee80211.Ieee80211MgmtFrameSerializer | behavior.change.fix | test whatsnew --- .../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 5fb46ae01ad..74fb6ef6266 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -35,8 +35,10 @@ Register_Serializer(Ieee80211ReassociationResponseFrame, Ieee80211MgmtFrameSeria static constexpr uint8_t HT_CAPABILITIES_ELEMENT_ID = 45; static constexpr uint8_t DSSS_PARAMETER_SET_ELEMENT_ID = 3; 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; @@ -104,6 +106,23 @@ static void readUnmodelledElement(MemoryInputStream& stream, const PtrsetUnmodelledElementPositions(index, precedingModelledElementCount); } +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 @@ -658,12 +677,8 @@ 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); elements.beginModelledElement(); - 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, elements, probeRequestFrame); writeHtElements(stream, elements, probeRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -680,13 +695,8 @@ 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, ???); elements.beginModelledElement(); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, reassociationRequestFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, elements, reassociationRequestFrame); writeHtElements(stream, elements, reassociationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -704,12 +714,8 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 2 Listen interval stream.writeUint16Le(associationRequestFrame->getListenInterval()); // 3 SSID - const char *SSID = associationRequestFrame->getSSID(); - unsigned int length = strlen(SSID); elements.beginModelledElement(); - 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, elements, associationRequestFrame); writeHtElements(stream, elements, associationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -760,12 +766,8 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Capability stream.writeUint16Le(beaconFrame->getCapabilityInformation()); // 4 Service Set Identifier (SSID) - const char *SSID = beaconFrame->getSSID(); - unsigned int length = strlen(SSID); elements.beginModelledElement(); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, beaconFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, elements, beaconFrame); writeHtElements(stream, elements, beaconFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); @@ -799,12 +801,8 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Capability stream.writeUint16Le(probeResponseFrame->getCapabilityInformation()); // 4 SSID - const char *SSID = probeResponseFrame->getSSID(); - unsigned int length = strlen(SSID); elements.beginModelledElement(); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, probeResponseFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, elements, probeResponseFrame); writeHtElements(stream, elements, 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 00010610bcea90add33c5acb3ca4a37fdd19089f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:56:08 +0200 Subject: [PATCH 17/21] ieee80211: fix: 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 success AIDs and nonzero failure AIDs. Change: src.ieee80211.Ieee80211MgmtFrameSerializer | behavior.change.fix | test whatsnew --- .../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 74fb6ef6266..8e52bac0b39 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -630,18 +630,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; } @@ -897,7 +895,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre auto frame = makeShared(); frame->setCapabilityInformation(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); @@ -909,7 +907,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre auto frame = makeShared(); frame->setCapabilityInformation(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 18e3638f495..439fce74a40 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 b17b7e9bf3526ed1d172f750abffb56bba01e998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:56:59 +0200 Subject: [PATCH 18/21] ieee80211: add: expose VHT MCS identity through the mode contract Consumers need a typed VHT MCS identity independent of the HT bitmap and concrete PHY classes. Add getVhtMcsIndex() to the mode contract, return the VHT index from VHT modes and provide a non-VHT default in the common base. Document the requirement for external mode implementations. Change: src.ieee80211 | behavior.add | test whatsnew migration --- doc/src/migration-guide/index.rst | 8 ++++++++ .../wireless/ieee80211/mode/IIeee80211Mode.h | 2 ++ .../wireless/ieee80211/mode/Ieee80211ModeBase.h | 1 + .../wireless/ieee80211/mode/Ieee80211VhtMode.h | 1 + 4 files changed, 12 insertions(+) diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index e0c05277abe..c42ad13ebca 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -182,6 +182,14 @@ 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 VHT Catalogs and Peer Rate Selection +---------------------------------------------- + +External ``IIeee80211Mode`` implementations must implement ``getVhtMcsIndex()``: +return the VHT MCS index (0 through 9), or -1 for other PHY families. +``Ieee80211ModeBase`` supplies the non-VHT default. VHT selection is independent +of the HT MCS bitmap. + Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index 64064fb19f1..9af016b4032 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -60,6 +60,8 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject // other PHY generations. HT capability derivation must use this typed // mode contract rather than concrete-type or name-based inference. virtual int getHtMcsIndex() const = 0; + // VHT MCS index (0..9), or -1 for other PHY generations. + virtual int getVhtMcsIndex() const = 0; // Returns whether this mode uses the optional 400 ns HT guard interval. // Non-HT modes deliberately report false. virtual bool isHtShortGuardInterval() const = 0; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h index d3387f0a934..cb0fa7f3306 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h @@ -21,6 +21,7 @@ class INET_API Ieee80211ModeBase : public IIeee80211Mode public: Ieee80211ModeBase(const char *name) : name(name) {} virtual int getHtMcsIndex() const override { return -1; } + virtual int getVhtMcsIndex() const override { return -1; } virtual bool isHtShortGuardInterval() const override { return false; } virtual const char *getName() const override { return name.c_str(); } virtual const simtime_t getPreambleDuration() const override { return getPreambleMode()->getDuration(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index 7cc8b91ef3d..d1a820e8eca 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h @@ -250,6 +250,7 @@ class INET_API Ieee80211VhtDataMode : public IIeee80211DataMode, public Ieee8021 class INET_API Ieee80211VhtMode : public Ieee80211ModeBase { public: + virtual int getVhtMcsIndex() const override { return dataMode->getMcsIndex(); } enum BandMode { BAND_2_4GHZ, BAND_5GHZ From 1ea317ca5ce4373fad4039afdf69a42c2fb56b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 11:59:12 +0200 Subject: [PATCH 19/21] ieee80211: add: complete the supported VHT guard-interval catalog Provide both guard intervals for all 310 legal supported VHT tuples. Prefer historical entries for unqualified lookups so floating-point rate ties preserve previous choices. Retain mandatory/basic flags and reference choices. Cover tuple exclusions, qualified lookup, timing and historical selection in the catalog test. Change: src.ieee80211.Ieee80211ModeSet | behavior.add | test whatsnew --- WHATSNEW | 4 + doc/src/migration-guide/index.rst | 14 ++- .../ieee80211/mode/Ieee80211ModeSet.cc | 55 +++++++--- .../ieee80211/mode/Ieee80211ModeSet.h | 2 + tests/unit/Ieee80211VhtModeSet_1.test | 101 ++++++++++++++++++ 5 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 tests/unit/Ieee80211VhtModeSet_1.test diff --git a/WHATSNEW b/WHATSNEW index 1ebf6258759..eef067663d3 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -396,6 +396,10 @@ Notable backward compatible changes are the following: group rate becomes 24 Mbps in the default mixed legacy catalog. This changes airtime and fingerprints in affected existing scenarios. + The ac catalog now contains both 800 ns and 400 ns GI for all 310 supported + VHT width/NSS/MCS tuples. Existing unqualified lookups, mandatory/basic flags, + and reference modes retain their previous choices. + INET-4.7 (July 2026) — feature release diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index c42ad13ebca..3050896b4c6 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -182,9 +182,19 @@ 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 VHT Catalogs and Peer Rate Selection ----------------------------------------------- +Migrating VHT Catalogs +--------------------- + +The ``ac`` catalog provides both 800 ns and 400 ns GI for 310 legal VHT tuples +at 20/40/80/160 MHz and one through eight spatial streams. IEEE 802.11-2024, +21.5, Tables 21-29 through 21-60 exclude: 20 MHz MCS 9 except NSS 3 and 6; +80 MHz MCS 6 at NSS 3 and 7; 80 MHz MCS 9 at NSS 6; and 160 MHz MCS 9 at NSS 3. +The band/preamble envelope remains 5 GHz, mixed format. New variants are optional +catalog entries; historical mandatory/basic flags, reference/default modes, and +previously accepted unspecified-GI lookups are preserved. Explicit GI queries +can select either variant. This catalog does not establish operational support +for bonded primary/secondary channels. External ``IIeee80211Mode`` implementations must implement ``getVhtMcsIndex()``: return the VHT MCS index (0 through 9), or -1 for other PHY families. ``Ieee80211ModeBase`` supplies the non-VHT default. VHT selection is independent diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 906910a00e2..e27e289f704 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -221,7 +221,8 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, true }, { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, true } }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true), - Ieee80211ModeSet("ac", { + Ieee80211ModeSet("ac", []() { + std::vector entries { { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs1BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs2BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, @@ -537,9 +538,27 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { true, &Ieee80211OfdmCompliantModes::ofdmMode6MbpsCS20MHz, true }, { true, &Ieee80211OfdmCompliantModes::ofdmMode12MbpsCS20MHz, true }, { true, &Ieee80211OfdmCompliantModes::ofdmMode24MbpsCS20MHz, true }, + }; + // IEEE Std 802.11-2024, 21.5 and Tables 21-29 through 21-60: + // each valid VHT tuple supports long GI, with short GI optional. + // New variants have lower unqualified-lookup priority than historical + // entries, preserving choices even across floating-point rate ties. + // Existing mandatory/basic flags and reference choices remain. + const size_t originalSize = entries.size(); + for (size_t i = 0; i < originalSize; i++) { + const auto *data = dynamic_cast(entries[i].mode->getDataMode()); + if (data != nullptr) { + auto gi = data->getGuardIntervalType() == Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG ? + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT : Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG; + entries.push_back({false, Ieee80211VhtCompliantModes::getCompliantMode(data->getModulationAndCodingScheme(), + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, gi), false, false}); + } + } + return entries; + }(), // Intentional model limitation: unlike IEEE Std 802.11-2024, 11.38.1, // this VHT-only profile has no selectable HT modes. - }, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::VHT),}; }); + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::VHT),}; }); Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector entries, const IIeee80211Mode *referenceMode, PhyType phyType, bool htOperationSupported) : @@ -698,19 +717,27 @@ const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int 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) && - guardIntervalMatches) - { - return entries[index].mode; + // Preserve prior unqualified queries even when a newly added GI variant + // sorts earlier because its mathematically equal bitrate rounds differently. + for (bool preferred : {true, false}) { + for (size_t index = 0; index < entries.size(); index++) { + if (guardInterval < SIMTIME_ZERO && entries[index].isPreferredForUnqualifiedLookup != preferred) + continue; + 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) && + guardIntervalMatches) + { + return entries[index].mode; + } } + if (guardInterval >= SIMTIME_ZERO) + break; } return nullptr; } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index 0e1a3b6254f..b7b769105fe 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -34,6 +34,8 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject bool isMandatory; const IIeee80211Mode *mode; bool isLegacyOperational = false; + // Prefer historical catalog entries when no GI qualifier is supplied. + bool isPreferredForUnqualifiedLookup = true; }; struct EntryNetBitrateComparator { diff --git a/tests/unit/Ieee80211VhtModeSet_1.test b/tests/unit/Ieee80211VhtModeSet_1.test new file mode 100644 index 00000000000..80fc8544fea --- /dev/null +++ b/tests/unit/Ieee80211VhtModeSet_1.test @@ -0,0 +1,101 @@ +%description: +Every valid VHT tuple in the ac catalog has both guard intervals. Existing +unqualified bitrate lookup and mandatory/reference selections retain their +historical choices. The validity matrix follows IEEE 802.11-2024, 21.5, +Tables 21-29 through 21-60, including the ten unavailable tuples. + +%includes: +#include +#include +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" +using namespace inet; +using namespace inet::physicallayer; + +%global: +class HistoricalVhtCatalog : public Ieee80211ModeSet +{ + static std::vector historicalEntries(const Ieee80211ModeSet *catalog) + { + std::vector result; + for (int i = 0; i < catalog->getNumModes(); i++) { + auto mode = catalog->getMode(i); + auto data = dynamic_cast(mode->getDataMode()); + if (data == nullptr) + result.push_back({true, mode, true}); + else { + bool originalLong = data->getBandwidth() == MHz(20) && data->getNumberOfSpatialStreams() == 1; + if ((data->getGuardInterval() == SimTime(800, SIMTIME_NS)) == originalLong) + result.push_back({originalLong, mode}); + } + } + return result; + } + public: + HistoricalVhtCatalog(const Ieee80211ModeSet *catalog) : + Ieee80211ModeSet("historical-ac", historicalEntries(catalog), catalog->getReferenceMode(), PhyType::VHT) {} +}; + +%activity: +auto catalog = Ieee80211ModeSet::getModeSet("ac"); +HistoricalVhtCatalog historical(catalog); +std::map, std::array> tuples; +for (int i = 0; i < catalog->getNumModes(); i++) { + auto mode = catalog->getMode(i); + auto data = dynamic_cast(mode->getDataMode()); + if (data == nullptr) { + ASSERT(catalog->isMandatory(i)); + continue; + } + ASSERT(catalog->isMandatory(i) == (data->getBandwidth() == MHz(20) && + data->getNumberOfSpatialStreams() == 1 && data->getGuardInterval() == SimTime(800, SIMTIME_NS))); + int gi = data->getGuardInterval() == SimTime(400, SIMTIME_NS) ? 1 : 0; + ASSERT(data->getGuardInterval() == SimTime(gi ? 400 : 800, SIMTIME_NS)); + auto& pair = tuples[{(int)data->getBandwidth().get(), data->getNumberOfSpatialStreams(), (int)data->getMcsIndex()}]; + ASSERT(pair[gi] == nullptr); + pair[gi] = mode; + auto found = catalog->getMode(data->getNetBitrate(), data->getBandwidth(), data->getNumberOfSpatialStreams(), data->getGuardInterval()); + ASSERT(found == mode); +} +int valid = 0; +for (int width : {20, 40, 80, 160}) { + for (int nss = 1; nss <= 8; nss++) { + for (int mcs = 0; mcs <= 9; mcs++) { + bool legal = !(width == 20 && mcs == 9 && nss != 3 && nss != 6) && + !(width == 80 && mcs == 6 && (nss == 3 || nss == 7)) && + !(width == 160 && mcs == 9 && nss == 3) && + !(width == 80 && mcs == 9 && nss == 6); + auto it = tuples.find({width, nss, mcs}); + ASSERT((it != tuples.end()) == legal); + if (legal) { + valid++; + auto longMode = it->second[0]; + auto shortMode = it->second[1]; + ASSERT(longMode && shortMode); + ASSERT(longMode->getDataMode()->getDuration(B(1500)) > shortMode->getDataMode()->getDuration(B(1500))); + ASSERT(longMode->getHeaderMode()->getDuration() == shortMode->getHeaderMode()->getDuration()); + } + } + } +} +ASSERT(valid == 310 && catalog->getNumModes() == 623); +ASSERT(historical.getNumModes() == 313); +for (int i = 0; i < historical.getNumModes(); i++) { + auto data = historical.getMode(i)->getDataMode(); + auto rate = data->getNetBitrate(); + ASSERT(catalog->getMode(rate) == historical.getMode(rate)); + ASSERT(catalog->getMode(rate, data->getBandwidth(), data->getNumberOfSpatialStreams()) == + historical.getMode(rate, data->getBandwidth(), data->getNumberOfSpatialStreams())); +} +ASSERT(catalog->getReferenceMode() == Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG)); +ASSERT(catalog->getFastestMode() == historical.getFastestMode()); +ASSERT(catalog->getSlowestMode() == historical.getSlowestMode()); +ASSERT(catalog->getFastestMandatoryMode() == historical.getFastestMandatoryMode()); +ASSERT(catalog->getSlowestMandatoryMode() == historical.getSlowestMandatoryMode()); +ASSERT(catalog->getLegacyOperationalModes() == historical.getLegacyOperationalModes()); +std::cout << "VHT GI catalog: 310 legal tuples, both GIs, historical selection preserved\n"; + +%contains: stdout +VHT GI catalog: 310 legal tuples, both GIs, historical selection preserved From e4f236f954b77388f1e58368078a043efc97169d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 14 Sep 2026 12:00:27 +0200 Subject: [PATCH 20/21] ieee80211: add+change: negotiate VHT capabilities for rate selection VHT requests previously bypassed negotiated receive limits. Exchange typed VHT capability and operation elements through detailed AP/STA management, commit peer state at association completion, and invalidate it on loss or mode-set application. Constrain DCF/HCF choices by local Tx and peer Rx maps, operating width, GI eligibility and long-GI rate limits. Missing negotiation falls back to legacy rates. Detailed operation remains primary-20 with long GI; document the retained VHT-only profile limitations. Update only lan80211ac/Ping1, run 0, 100s in examples.csv and the matching JSON entries: 8180-0d11/tplx becomes e382-0a32/tplx and a5d5-2820/~tNl becomes 45db-b12b/~tNl. Without negotiated VHT state, the first unicast now uses 24 Mbps OFDM, reducing airtime from 72 us to 44 us. The isolated catalog-only run preserves the old fingerprints, identifying peer filtering as the cause. The new values were explicitly approved. The migration guide references the MIB parameter definitions and retains a directional MCS-map example instead of duplicating defaults. Expose virtual VHT advertisement helpers so inherited AP/STA builders honor management subclass overrides. Apply subtype permissions independently to HT and VHT presence bits in both serializer directions; association requests must not carry a VHT operation element. Cover both contracts in focused fixtures and document the normative limits used by the peer selector. Plan: plan/done/ht-gi-devin-comment-closure.md Change: src.ieee80211 | behavior.add+change | test whatsnew migration fingerprint --- WHATSNEW | 11 + doc/src/migration-guide/index.rst | 46 ++- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 48 +++ .../linklayer/ieee80211/mac/Ieee80211Mac.h | 1 + .../Ieee80211PeerModeSelection.cc | 81 +++++ .../Ieee80211PeerModeSelection.h | 7 + .../mac/rateselection/QosRateSelection.cc | 11 +- .../mac/rateselection/RateSelection.cc | 11 +- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 65 +++- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 3 + .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 13 + .../ieee80211/mgmt/Ieee80211MgmtBase.h | 4 + .../ieee80211/mgmt/Ieee80211MgmtFrame.msg | 27 +- .../mgmt/Ieee80211MgmtFrameSerializer.cc | 181 ++++++++-- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 54 ++- .../ieee80211/mgmt/Ieee80211MgmtSta.h | 4 + .../ieee80211/mgmt/Ieee80211VhtMgmtElements.h | 106 ++++++ .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 60 +++- .../linklayer/ieee80211/mib/Ieee80211Mib.h | 21 ++ .../linklayer/ieee80211/mib/Ieee80211Mib.ned | 4 + .../ieee80211/mib/Ieee80211VhtCapabilities.h | 65 ++++ tests/fingerprint/examples.csv | 2 +- tests/fingerprint/store.json | 4 +- tests/module/Ieee80211VhtAssociation_1.test | 323 ++++++++++++++++++ tests/unit/Ieee80211VhtMgmtElements_1.test | 176 ++++++++++ .../unit/Ieee80211VhtPeerModeSelection_1.test | 77 +++++ 26 files changed, 1337 insertions(+), 68 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h create mode 100644 src/inet/linklayer/ieee80211/mib/Ieee80211VhtCapabilities.h create mode 100644 tests/module/Ieee80211VhtAssociation_1.test create mode 100644 tests/unit/Ieee80211VhtMgmtElements_1.test create mode 100644 tests/unit/Ieee80211VhtPeerModeSelection_1.test diff --git a/WHATSNEW b/WHATSNEW index eef067663d3..11a6544efaa 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -390,6 +390,9 @@ Notable backward compatible changes are the following: changes and reapplying unchanged settings no longer interrupt reception. Incompatible changes abort through the normal reception cleanup path. + Management subclasses can override addVhtCapabilities and addVhtOperation + to customize advertisements produced by inherited AP/STA frame builders. + 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 @@ -400,6 +403,14 @@ Notable backward compatible changes are the following: VHT width/NSS/MCS tuples. Existing unqualified lookups, mandatory/basic flags, and reference modes retain their previous choices. + Detailed AP/STA management now exchanges VHT Capabilities and VHT Operation + elements. Both rate selectors constrain VHT unicast by local Tx and peer Rx + MCS/NSS maps, bandwidth, GI, and advertised long-GI rate limits. Association + loss and mode-set application invalidate peer state. Missing VHT negotiation + falls back to legacy rates, including simplified-management and ad-hoc setups; + this changes their airtime and fingerprints. Current detailed VHT operation + is limited to 20 MHz and long GI. See the migration guide for configuration + and the retained VHT-only profile limitations. INET-4.7 (July 2026) — feature release diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 3050896b4c6..f358c9ca9b3 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -105,8 +105,8 @@ answers; :cpp:`Arp` shows how. An implementation that resolves addresses without packets has nobody to ask. It overrides the method with an empty body, as :cpp:`GlobalArp` does, and the client then takes the address. -IEEE 802.11 Radio Reconfiguration --------------------------------- +IEEE 802.11 Radio Reconfiguration and Management Hooks +---------------------------------------------------- Radio setters no longer implicitly interrupt compatible ongoing receptions. Changing the transmit mode alone, reapplying unchanged receiver settings, or @@ -115,6 +115,12 @@ An incompatible receiver configuration still aborts reception and retains arrival timers for normal cleanup. Custom callers should not rely on a no-op setter or a transmit-mode change to cancel reception. +``Ieee80211MgmtBase::addVhtCapabilities()`` and ``addVhtOperation()`` are now +virtual, like the HT advertisement helpers. Subclasses may override them to +customize advertisements in inherited frame builders; use ``override`` on +these declarations. Rebuild external management subclasses against the new +header and library. + Migrating IEEE 802.11 PHY Modes ------------------------------ @@ -183,8 +189,8 @@ 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 VHT Catalogs ---------------------- +Migrating VHT Catalogs and Peer Rate Selection +---------------------------------------------- The ``ac`` catalog provides both 800 ns and 400 ns GI for 310 legal VHT tuples at 20/40/80/160 MHz and one through eight spatial streams. IEEE 802.11-2024, @@ -195,11 +201,43 @@ catalog entries; historical mandatory/basic flags, reference/default modes, and previously accepted unspecified-GI lookups are preserved. Explicit GI queries can select either variant. This catalog does not establish operational support for bonded primary/secondary channels. + External ``IIeee80211Mode`` implementations must implement ``getVhtMcsIndex()``: return the VHT MCS index (0 through 9), or -1 for other PHY families. ``Ieee80211ModeBase`` supplies the non-VHT default. VHT selection is independent of the HT MCS bitmap. +``Ieee80211MgmtAp`` and ``Ieee80211MgmtSta`` now exchange and interpret VHT +Capabilities and VHT Operation elements (IEEE 802.11-2024, 9.4.2.156 and +9.4.2.157). The MIB owns committed per-peer state. The AP commits after the +successful association/reassociation response is acknowledged; the STA commits +after receiving a successful response with usable capability and operation +information. Pending VHT snapshots cannot survive a local mode-set application. +Disassociation, deauthentication, teardown, and mode-set application remove +committed state. Authoritative beacons can refresh the associated AP's state. + +To restrict VHT reception or transmission, configure the corresponding map in +:ned:`Ieee80211Mib`; that module documents the parameters and their constraints. +For example, ``wlan[*].mib.vhtRxMcsMap = [7,-1,-1,-1,-1,-1,-1,-1]`` restricts +reception to one stream with MCS 0 through 7 while leaving transmission +configuration independent. + +Both DCF and HCF choose VHT unicast modes within local Tx and peer Rx maps, +local/BSS operation width, GI eligibility, and the optional advertised highest +long-GI rate limits. Selection never exceeds the requested rate. Missing or +incompatible VHT negotiation uses a legacy operational mode. Consequently, +``Ieee80211MgmtApSimplified``, ``Ieee80211MgmtStaSimplified``, and ad-hoc +compositions use legacy unicast until a management implementation supplies +valid VHT peer state. Existing VHT results, including ``lan80211ac/Ping1``, can +change even though unspecified-GI catalog lookup is preserved. + +The current packet-level detailed-management support envelope is 20 MHz with +long GI. The existing ``ac`` profile remains VHT-only and does not supply the +HT modes required for full standards-conforming VHT operation. In particular, +it does not negotiate HT-carried short-GI bits for 20/40 MHz. Wider catalog and +selector tests do not claim bonded-channel operation. MU, beamforming, 80+80, +extended NSS bandwidth signaling, and operating-mode notifications are not +implemented by this change. Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index f48b185a9ac..a9454bcd533 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -98,6 +98,7 @@ void Ieee80211Mac::prepareLocalCapabilities() void Ieee80211Mac::updateLocalHtCapabilities(bool reconfiguration) { + updateLocalVhtCapabilities(); if (!modeSet->isHtOperationSupported()) { if (reconfiguration) mib->reconfigureLocalHtCapabilities(Ieee80211HtCapabilities(), false); @@ -161,6 +162,53 @@ void Ieee80211Mac::updateLocalHtCapabilities(bool reconfiguration) mib->installLocalHtCapabilities(localHtCapabilities, true); } +void Ieee80211Mac::updateLocalVhtCapabilities() +{ + Ieee80211VhtCapabilities localVhtCapabilities; + bool supported = modeSet != nullptr && modeSet->getPhyType() == Ieee80211ModeSet::PhyType::VHT && mib->par("vhtSupported"); + if (!supported) { + mib->installLocalVhtCapabilities(localVhtCapabilities, false); + return; + } + int spatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), modeSet->getMaximumNumberOfSpatialStreams()); + // Advertised maps are bounded by the actual long-GI primary-20 catalog, + // as well as the configured directional and antenna limits. + std::array, 8> catalogMcs = {}; + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto mode = modeSet->getMode(i); + auto data = mode->getDataMode(); + int mcs = mode->getVhtMcsIndex(); + int nss = data->getNumberOfSpatialStreams(); + if (mcs >= 0 && mcs <= 9 && nss >= 1 && nss <= 8 && data->getBandwidth() == MHz(20) && + data->getGuardInterval() == SimTime(800, SIMTIME_NS)) + catalogMcs[nss - 1][mcs] = true; + } + auto readMap = [&](const char *parameter, std::array& map) { + auto values = check_and_cast(mib->par(parameter).objectValue()); + if (values->size() != 8) + throw cRuntimeError("%s requires eight per-NSS MCS maxima", parameter); + for (int i = 0; i < 8; i++) { + int value = values->get(i).intValue(); + if (value != -1 && value != 7 && value != 8 && value != 9) + throw cRuntimeError("%s entries must be -1, 7, 8 or 9", parameter); + int maximum = -1; + for (int mcs = 0; mcs <= value && catalogMcs[i][mcs]; mcs++) + if (mcs >= 7) + maximum = mcs; + map[i] = i < spatialStreamLimit ? maximum : -1; + } + if (!isValidVhtMcsMap(map)) + throw cRuntimeError("%s and the VHT catalog must support MCS 0 through 7 at one spatial stream", parameter); + }; + readMap("vhtRxMcsMap", localVhtCapabilities.rxMaxMcs); + readMap("vhtTxMcsMap", localVhtCapabilities.txMaxMcs); + // Intentional limitation of the current packet-level primary-channel PHY: + // management operates the existing VHT-only profile at 20 MHz. Its catalog + // is broader, but does not establish primary/secondary channel support. + // No HT SGI negotiation is claimed by that profile, so 20 MHz uses long GI. + mib->installLocalVhtCapabilities(localVhtCapabilities, true); +} + void Ieee80211Mac::initializeRadioMode() { const char *initialRadioMode = par("initialRadioMode"); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 65b89e247d7..387eee5546a 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -80,6 +80,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfig virtual void initialize(int) override; virtual void initializeRadioMode(); void updateLocalHtCapabilities(bool reconfiguration = false); + void updateLocalVhtCapabilities(); virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 5dfaf17d0bd..7d9751369ae 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -5,6 +5,7 @@ // +#include #include #include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" @@ -153,5 +154,85 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, return bestMode != nullptr ? bestMode : getLegacyFallback(modeSet, mode, peerAddress); } +const IIeee80211Mode *selectPeerCompatibleVhtMode(const Ieee80211ModeSet *modeSet, + const Ieee80211VhtCapabilities& local, const Ieee80211VhtOperation& localOperation, + const Ieee80211Mib::PeerVhtState *peer, const IIeee80211Mode *requested) +{ + // IEEE Std 802.11-2024, 10.6.13.1 and 10.6.13.2 define directional + // VHT MCS/NSS and long-GI rate support. Width capability and BSS width + // are described in 9.4.2.156.2 (Table 9-313) and 11.38.1. + // The requested-rate ceiling, highest-rate choice and legacy fallback + // are model policy, not a prescribed IEEE rate-control algorithm. + if (requested == nullptr || requested->getVhtMcsIndex() < 0) + return requested; + if (modeSet == nullptr) + throw cRuntimeError("Cannot select a VHT mode without a mode set"); + const IIeee80211Mode *best = nullptr; + auto ceiling = requested->getDataMode()->getNetBitrate(); + if (peer != nullptr) { + const auto& remote = peer->advertisedCapabilities; + auto compatible = [&](const IIeee80211Mode *mode) { + int mcs = mode->getVhtMcsIndex(); + auto data = mode->getDataMode(); + int nss = data->getNumberOfSpatialStreams(); + auto width = data->getBandwidth(); + if (mcs < 0 || nss < 1 || nss > 8 || local.txMaxMcs[nss - 1] < mcs || remote.rxMaxMcs[nss - 1] < mcs || + width > localOperation.channelWidth || width > peer->operation.channelWidth || + (width > MHz(80) && (!local.supported160Mhz || !remote.supported160Mhz))) + return false; + // IEEE Std 802.11-2024, 10.17: receiver-advertised short GI for + // the selected width and local activation are both required. + // This model uses local capability flags as its activation limits. + bool shortGi = data->getGuardInterval() == SimTime(400, SIMTIME_NS); + bool giSupported = width == MHz(20) ? local.shortGi20 && remote.shortGi20 : + width == MHz(40) ? local.shortGi40 && remote.shortGi40 : + width == MHz(80) ? local.shortGi80 && remote.shortGi80 : local.shortGi160 && remote.shortGi160; + if (shortGi && !giSupported) + return false; + // Highest Supported Long GI Data Rate limits refer to long-GI rate, + // including when selecting a corresponding short-GI transmission. + auto longGiRate = data->getNetBitrate(); + if (shortGi) { + bool found = false; + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto counterpart = modeSet->getMode(i); + auto counterpartData = counterpart->getDataMode(); + if (counterpart->getVhtMcsIndex() == mcs && counterpartData->getBandwidth() == width && + counterpartData->getNumberOfSpatialStreams() == nss && + counterpartData->getGuardInterval() == SimTime(800, SIMTIME_NS)) { + longGiRate = counterpartData->getNetBitrate(); + found = true; + break; + } + } + if (!found) + return false; + } + // IEEE Std 802.11-2024, 10.6.13.1/.2 and Table 9-315: + // compare floor(long-GI rate in Mb/s), also for short-GI modes. + int encodedRate = std::floor(longGiRate.get()); + return (local.txHighestLongGiRateMbps == 0 || encodedRate <= local.txHighestLongGiRateMbps) && + (remote.rxHighestLongGiRateMbps == 0 || encodedRate <= remote.rxHighestLongGiRateMbps); + }; + if (modeSet->containsMode(requested) && compatible(requested)) + return requested; + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto mode = modeSet->getMode(i); + if (mode->getDataMode()->getNetBitrate() <= ceiling && compatible(mode) && + (best == nullptr || mode->getDataMode()->getNetBitrate() > best->getDataMode()->getNetBitrate())) + best = mode; + } + } + if (best != nullptr) + return best; + for (auto mode : modeSet->getLegacyOperationalModes()) + if (mode->getDataMode()->getNetBitrate() <= ceiling && + (best == nullptr || mode->getDataMode()->getNetBitrate() > best->getDataMode()->getNetBitrate())) + best = mode; + if (best == nullptr) + throw cRuntimeError("No legacy fallback for unnegotiated VHT mode"); + return best; +} + } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index 6c6543576cf..ee2547fb299 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -30,6 +30,13 @@ INET_API const physicallayer::IIeee80211Mode *selectPeerCompatibleMode( const MacAddress& peerAddress, const Ieee80211HtOperation *operation, bool htEligible); +// VHT selection intersects local Tx and peer Rx; absent negotiation uses legacy. +INET_API const physicallayer::IIeee80211Mode *selectPeerCompatibleVhtMode( + const physicallayer::Ieee80211ModeSet *modeSet, + const Ieee80211VhtCapabilities& local, const Ieee80211VhtOperation& localOperation, + const Ieee80211Mib::PeerVhtState *peer, + const physicallayer::IIeee80211Mode *requested); + } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 532812e5902..ab2226e6838 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -265,11 +265,12 @@ void QosRateSelection::frameTransmitted(Packet *packet, const PtrgetHtMcsIndex() < 0) + if (mode == nullptr || peerAddress.isMulticast() || !mib) + return mode; + if (mode->getVhtMcsIndex() >= 0) + return selectPeerCompatibleVhtMode(modeSet, mib->getLocalVhtCapabilities(), mib->getLocalVhtOperation(), + mib->findPeerVhtState(peerAddress), mode); + if (mode->getHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerCapabilities(peerAddress), mode, peerAddress, mib->hasHtOperation() ? &mib->getHtOperation() : nullptr, mib->relationshipAllowsHt(peerAddress)); diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 648e1378e8c..65c70a74557 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -231,11 +231,12 @@ void RateSelection::emitDatarateSelected(cComponent *emitter, const PtrgetHtMcsIndex() < 0) + if (mode == nullptr || peerAddress.isMulticast() || !mib) + return mode; + if (mode->getVhtMcsIndex() >= 0) + return selectPeerCompatibleVhtMode(modeSet, mib->getLocalVhtCapabilities(), mib->getLocalVhtOperation(), + mib->findPeerVhtState(peerAddress), mode); + if (mode->getHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerCapabilities(peerAddress), mode, peerAddress, mib->hasHtOperation() ? &mib->getHtOperation() : nullptr, mib->relationshipAllowsHt(peerAddress)); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index c3e3106e75b..9d9bb33399f 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -18,6 +18,7 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211BeaconInterval.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -131,7 +132,7 @@ void Ieee80211MgmtAp::frameTransmissionFinished(const Packet *responseFrame, Fra if (sta->second.pendingAssociationSuccessful) { bool wasAssociated = mib->getPeerAssociationStatus(address) == Ieee80211Mib::ASSOCIATED; // An acknowledged replacement starts a new relationship, even for equal capabilities. - mib->removePeerHtCapabilities(address); + mib->removePeerCapabilities(address); mib->commitAssociationId(address); mib->setPeerAssociationStatus(address, Ieee80211Mib::ASSOCIATED); if (sta->second.pendingHtStateAvailable) { @@ -139,8 +140,13 @@ void Ieee80211MgmtAp::frameTransmissionFinished(const Packet *responseFrame, Fra if (sta->second.pendingHtCapabilitiesValid && mib->isLocalHtCapable()) mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities); else - mib->removePeerHtCapabilities(address); + mib->removePeerCapabilities(address); } + if (sta->second.pendingVhtCapabilitiesValid && mib->isVhtOperationSupported() && + sta->second.pendingVhtGeneration == mib->getVhtCapabilityGeneration()) + mib->setPeerVhtCapabilities(address, sta->second.pendingVhtCapabilities, mib->getLocalVhtOperation()); + else + mib->removePeerVhtCapabilities(address); clearPendingAssociation(&sta->second); mib->publishStateChange(); // Signal delivery is synchronous; observers must see committed @@ -197,6 +203,9 @@ void Ieee80211MgmtAp::clearPendingAssociation(StaInfo *sta) mib->cancelAssociationIdReservation(sta->address); sta->pendingAssociationSuccessful = false; sta->pendingAssociationTransactionId = 0; + sta->pendingVhtCapabilitiesValid = false; + sta->pendingVhtCapabilities = Ieee80211VhtCapabilities(); + sta->pendingVhtGeneration = 0; sta->pendingHtStateAvailable = false; sta->pendingHtCapabilitiesValid = false; sta->pendingHtCapabilities = Ieee80211HtCapabilities(); @@ -213,9 +222,11 @@ void Ieee80211MgmtAp::sendBeacon() body->setBeaconInterval(beaconInterval); body->setChannelNumber(getDsssParameterSetChannel()); addHtCapabilities(body); + addVhtCapabilities(body); + addVhtOperation(body); if (mib->isLocalHtCapable()) setHtOperation(body, getHtOperationBand(), mib->getHtOperation()); - body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (body->getChannelNumber() != -1 ? 3 : 0)) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); + body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (body->getChannelNumber() != -1 ? 3 : 0)) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body) + getVhtMgmtElementsLength(body)); sendManagementFrame("Beacon", body, ST_BEACON, MacAddress::BROADCAST_ADDRESS); } @@ -251,7 +262,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::NOT_AUTHENTICATED); - mib->removePeerHtCapabilities(sta->address); + mib->removePeerCapabilities(sta->address); sta->authSeqExpected = 1; if (wasAssociated) sendDisAssocNotification(sta->address); @@ -290,7 +301,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::AUTHENTICATED); // TODO only when ACK of this frame arrives - mib->removePeerHtCapabilities(sta->address); + mib->removePeerCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); EV << "STA authenticated\n"; @@ -316,7 +327,7 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::NOT_AUTHENTICATED); sta->authSeqExpected = 1; - mib->removePeerHtCapabilities(sta->address); + mib->removePeerCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); } @@ -365,6 +376,13 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrisVhtOperationSupported() && + decodeVhtCapabilities(requestBody, pendingVhtCapabilities); + bool vhtCapabilitiesMalformed = mib->isVhtOperationSupported() && + requestBody->getVhtCapabilitiesPresent() && !pendingVhtCapabilitiesValid; + bool basicVhtMcsSupported = !pendingVhtCapabilitiesValid || + supportsBasicVhtMcsSet(pendingVhtCapabilities, mib->getLocalVhtOperation()); bool basicHtMcsSupported = !htCapabilitiesMalformed && (!pendingHtCapabilitiesValid || supportsBasicHtMcsSet(pendingHtCapabilities, pendingHtOperation)); delete packet; @@ -379,13 +397,16 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrsetStatusCode(statusCode); bool associationSuccessful = statusCode == SC_SUCCESSFUL; short associationId = associationSuccessful ? mib->reserveAssociationId(sta->address) : 0; body->setAid(associationId); sta->pendingAssociationSuccessful = associationSuccessful; + sta->pendingVhtCapabilitiesValid = pendingVhtCapabilitiesValid; + sta->pendingVhtCapabilities = pendingVhtCapabilities; + sta->pendingVhtGeneration = mib->getVhtCapabilityGeneration(); sta->pendingHtStateAvailable = true; sta->pendingHtCapabilitiesValid = pendingHtCapabilitiesValid; sta->pendingHtCapabilities = pendingHtCapabilities; @@ -394,7 +415,9 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrpendingAssociationTransactionId = createAssociationTransactionId(); setSupportedRateElements(body); addHtCapabilities(body); - body->setChunkLength(B(2 + 2 + 2) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); + addVhtCapabilities(body); + addVhtOperation(body); + body->setChunkLength(B(2 + 2 + 2) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body) + getVhtMgmtElementsLength(body)); const char *frameName = associationSuccessful ? "AssocResp-OK" : (htCapabilitiesMalformed ? "AssocResp-UnsupportedHtCap" : "AssocResp-UnsupportedHtMcs"); sendManagementFrame(frameName, body, ST_ASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); @@ -445,6 +468,13 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< htCapabilitiesMalformed = true; } } + Ieee80211VhtCapabilities pendingVhtCapabilities; + bool pendingVhtCapabilitiesValid = mib->isVhtOperationSupported() && + decodeVhtCapabilities(requestBody, pendingVhtCapabilities); + bool vhtCapabilitiesMalformed = mib->isVhtOperationSupported() && + requestBody->getVhtCapabilitiesPresent() && !pendingVhtCapabilitiesValid; + bool basicVhtMcsSupported = !pendingVhtCapabilitiesValid || + supportsBasicVhtMcsSet(pendingVhtCapabilities, mib->getLocalVhtOperation()); bool basicHtMcsSupported = !htCapabilitiesMalformed && (!pendingHtCapabilitiesValid || supportsBasicHtMcsSet(pendingHtCapabilities, pendingHtOperation)); delete packet; @@ -458,13 +488,16 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< responseHtOperation.basicMcsSupported.fill(false); setHtOperation(body, getHtOperationBand(), responseHtOperation); } - Ieee80211StatusCode statusCode = htCapabilitiesMalformed ? SC_UNSUP_CAP : - (basicHtMcsSupported ? SC_SUCCESSFUL : SC_DATARATE_UNSUP); + Ieee80211StatusCode statusCode = htCapabilitiesMalformed || vhtCapabilitiesMalformed ? SC_UNSUP_CAP : + (basicHtMcsSupported && basicVhtMcsSupported ? SC_SUCCESSFUL : SC_DATARATE_UNSUP); body->setStatusCode(statusCode); bool associationSuccessful = statusCode == SC_SUCCESSFUL; short associationId = associationSuccessful ? mib->reserveAssociationId(sta->address) : 0; body->setAid(associationId); sta->pendingAssociationSuccessful = associationSuccessful; + sta->pendingVhtCapabilitiesValid = pendingVhtCapabilitiesValid; + sta->pendingVhtCapabilities = pendingVhtCapabilities; + sta->pendingVhtGeneration = mib->getVhtCapabilityGeneration(); sta->pendingHtStateAvailable = true; sta->pendingHtCapabilitiesValid = pendingHtCapabilitiesValid; sta->pendingHtCapabilities = pendingHtCapabilities; @@ -473,7 +506,9 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< sta->pendingAssociationTransactionId = createAssociationTransactionId(); setSupportedRateElements(body); addHtCapabilities(body); - body->setChunkLength(B(2 + 2 + 2) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); + addVhtCapabilities(body); + addVhtOperation(body); + body->setChunkLength(B(2 + 2 + 2) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body) + getVhtMgmtElementsLength(body)); const char *frameName = associationSuccessful ? "ReassocResp-OK" : (htCapabilitiesMalformed ? "ReassocResp-UnsupportedHtCap" : "ReassocResp-UnsupportedHtMcs"); sendManagementFrame(frameName, body, ST_REASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); @@ -495,7 +530,7 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::AUTHENTICATED); - mib->removePeerHtCapabilities(sta->address); + mib->removePeerCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); } @@ -527,9 +562,11 @@ void Ieee80211MgmtAp::handleProbeRequestFrame(Packet *packet, const PtrsetBeaconInterval(beaconInterval); body->setChannelNumber(getDsssParameterSetChannel()); addHtCapabilities(body); + addVhtCapabilities(body); + addVhtOperation(body); if (mib->isLocalHtCapable()) setHtOperation(body, getHtOperationBand(), mib->getHtOperation()); - body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (body->getChannelNumber() != -1 ? 3 : 0)) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); + body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (body->getChannelNumber() != -1 ? 3 : 0)) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body) + getVhtMgmtElementsLength(body)); sendManagementFrame("ProbeResp", body, ST_PROBERESPONSE, staAddress); } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index fe8c0c03192..9d65a8eb8d6 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -37,6 +37,9 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase int authSeqExpected; // when NOT_AUTHENTICATED: transaction sequence number of next expected auth frame bool pendingAssociationSuccessful = false; uint64_t pendingAssociationTransactionId = 0; + bool pendingVhtCapabilitiesValid = false; + Ieee80211VhtCapabilities pendingVhtCapabilities; + uint64_t pendingVhtGeneration = 0; bool pendingHtStateAvailable = false; bool pendingHtCapabilitiesValid = false; Ieee80211HtCapabilities pendingHtCapabilities; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index cbcd3191a80..8023005a4c0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -16,6 +16,7 @@ #include "inet/common/lifecycle/NodeStatus.h" #include "inet/linklayer/common/InterfaceTag_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -130,6 +131,18 @@ void Ieee80211MgmtBase::prepareLocalOperation() } +void Ieee80211MgmtBase::addVhtCapabilities(const Ptr& frame) const +{ + if (mib->isVhtOperationSupported()) + setVhtCapabilities(frame, mib->getLocalVhtCapabilities()); +} + +void Ieee80211MgmtBase::addVhtOperation(const Ptr& frame) const +{ + if (mib->isVhtOperationSupported()) + setVhtOperation(frame, mib->getLocalVhtOperation()); +} + void Ieee80211MgmtBase::addHtCapabilities(const Ptr& frame) const { if (mib->isLocalHtCapable()) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index 9ff181c18d0..73d9b5f5486 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -91,6 +91,10 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener, pub return length; } + /** Adds local VHT capabilities; subclasses may customize advertisements in inherited frame builders. */ + virtual void addVhtCapabilities(const Ptr& frame) const; + /** Adds local VHT operation; subclasses may customize advertisements in inherited frame builders. */ + virtual void addVhtOperation(const Ptr& frame) const; /** Adds the local HT advertisement to a frame when the authoritative PHY profile supports HT operation. */ Ieee80211HtOperation computeLocalHtOperation(int primaryChannel, const physicallayer::IIeee80211Band *band) const; virtual void prepareLocalOperation(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg index 4725c5e1110..2e4f4375342 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg @@ -180,6 +180,27 @@ struct Ieee80211HtOperationElement // // Frame body base class used to hide various frame body types // +// IEEE Std 802.11-2024, 9.4.2.156: modeled VHT Capabilities fields. +struct Ieee80211VhtCapabilitiesElement +{ + short supportedChannelWidthSet; + bool shortGi80; + bool shortGi160; + short rxMaxMcs[8]; + short txMaxMcs[8]; + short rxHighestLongGiRateMbps; + short txHighestLongGiRateMbps; +} + +// IEEE Std 802.11-2024, 9.4.2.157: fixed five-octet VHT Operation body. +struct Ieee80211VhtOperationElement +{ + short channelWidth; + short centerFrequencySegment0; + short centerFrequencySegment1; + short basicMaxMcs[8]; +} + class Ieee80211MgmtFrame extends FieldsChunk { bool extendedSupportedRatesPresent; @@ -188,13 +209,17 @@ class Ieee80211MgmtFrame extends FieldsChunk Ieee80211HtCapabilitiesElement htCapabilities; bool htOperationPresent; Ieee80211HtOperationElement htOperation; + bool vhtCapabilitiesPresent; + Ieee80211VhtCapabilitiesElement vhtCapabilities; + bool vhtOperationPresent; + Ieee80211VhtOperationElement vhtOperation; // Information elements this model does not represent (TIM, Country, ERP, RSN, // vendor-specific, ...), each kept verbatim as Element ID, Length and body, in wire // order. Empty for the frames the simulation builds itself. uint8_t unmodelledElements[]; // One entry per element in unmodelledElements: how many of the modelled elements // (SSID, Supported Rates, DSSS Parameter Set, Extended Supported Rates, HT - // Capabilities, HT Operation) preceded it on the wire, so that it is written back + // Capabilities, HT Operation, VHT Capabilities, VHT Operation) preceded it on the wire, so that it is written back // in its place among them. uint8_t unmodelledElementPositions[]; } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 8e52bac0b39..ff3d63145d9 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -331,15 +331,114 @@ static void writeHtOperationElement(MemoryOutputStream& stream, const Ieee80211H stream.writeByte(byte); } -enum HtElementPresence : unsigned int { +// IEEE Std 802.11-2024, 9.4.2.156 and 9.4.2.157. +static constexpr uint8_t VHT_CAPABILITIES_ELEMENT_ID = 191; +static constexpr uint8_t VHT_OPERATION_ELEMENT_ID = 192; + +static uint16_t encodeVhtMcsMap(const short *map) +{ + uint16_t result = 0; + for (int i = 0; i < 8; i++) { + int value = map[i]; + if (value != -1 && value != 7 && value != 8 && value != 9) + throw cRuntimeError("Invalid VHT MCS map entry %d", value); + result |= (value == -1 ? 3 : value - 7) << (2 * i); + } + return result; +} + +static void decodeVhtMcsMap(uint16_t encoded, short *map) +{ + for (int i = 0; i < 8; i++) { + int value = (encoded >> (2 * i)) & 3; + map[i] = value == 3 ? -1 : value + 7; + } +} + +static void writeVhtCapabilitiesElement(MemoryOutputStream& stream, const Ieee80211VhtCapabilitiesElement& capabilities) +{ + if (capabilities.supportedChannelWidthSet < 0 || capabilities.supportedChannelWidthSet > 2 || + capabilities.rxHighestLongGiRateMbps < 0 || capabilities.rxHighestLongGiRateMbps > 8191 || + capabilities.txHighestLongGiRateMbps < 0 || capabilities.txHighestLongGiRateMbps > 8191) + throw cRuntimeError("Invalid VHT Capabilities fields"); + stream.writeByte(VHT_CAPABILITIES_ELEMENT_ID); + stream.writeByte(12); + stream.writeUint32Le((capabilities.supportedChannelWidthSet << 2) | + (capabilities.shortGi80 << 5) | (capabilities.shortGi160 << 6)); + stream.writeUint16Le(encodeVhtMcsMap(capabilities.rxMaxMcs)); + stream.writeUint16Le(capabilities.rxHighestLongGiRateMbps); + stream.writeUint16Le(encodeVhtMcsMap(capabilities.txMaxMcs)); + stream.writeUint16Le(capabilities.txHighestLongGiRateMbps); +} + +static void writeVhtOperationElement(MemoryOutputStream& stream, const Ieee80211VhtOperationElement& operation) +{ + if (operation.channelWidth < 0 || operation.channelWidth > 3 || + operation.centerFrequencySegment0 < 0 || operation.centerFrequencySegment0 > 255 || + operation.centerFrequencySegment1 < 0 || operation.centerFrequencySegment1 > 255) + throw cRuntimeError("Invalid VHT Operation fields"); + stream.writeByte(VHT_OPERATION_ELEMENT_ID); + stream.writeByte(5); + stream.writeByte(operation.channelWidth); + stream.writeByte(operation.centerFrequencySegment0); + stream.writeByte(operation.centerFrequencySegment1); + stream.writeUint16Le(encodeVhtMcsMap(operation.basicMaxMcs)); +} + +static void readVhtCapabilitiesElement(MemoryInputStream& stream, int length, const Ptr& frame) +{ + if (length != 12 || frame->getVhtCapabilitiesPresent()) { + frame->markIncorrect(); + stream.seek(stream.getPosition() + B(length)); + return; + } + Ieee80211VhtCapabilitiesElement capabilities; + auto information = stream.readUint32Le(); + capabilities.supportedChannelWidthSet = (information >> 2) & 3; + capabilities.shortGi80 = information & (1 << 5); + capabilities.shortGi160 = information & (1 << 6); + decodeVhtMcsMap(stream.readUint16Le(), capabilities.rxMaxMcs); + capabilities.rxHighestLongGiRateMbps = stream.readUint16Le() & 8191; + decodeVhtMcsMap(stream.readUint16Le(), capabilities.txMaxMcs); + capabilities.txHighestLongGiRateMbps = stream.readUint16Le() & 8191; + if (capabilities.supportedChannelWidthSet == 3) + frame->markIncorrect(); + frame->setVhtCapabilities(capabilities); + frame->setVhtCapabilitiesPresent(true); +} + +static void readVhtOperationElement(MemoryInputStream& stream, int length, const Ptr& frame) +{ + if (length != 5 || frame->getVhtOperationPresent()) { + frame->markIncorrect(); + stream.seek(stream.getPosition() + B(length)); + return; + } + Ieee80211VhtOperationElement operation; + operation.channelWidth = stream.readByte(); + operation.centerFrequencySegment0 = stream.readByte(); + operation.centerFrequencySegment1 = stream.readByte(); + decodeVhtMcsMap(stream.readUint16Le(), operation.basicMaxMcs); + if (operation.channelWidth > 3) + frame->markIncorrect(); + frame->setVhtOperation(operation); + frame->setVhtOperationPresent(true); +} + +// IEEE Std 802.11-2024, 9.3.3.2 and 9.3.3.5-9.3.3.10, Tables 9-62 and +// 9-64 through 9-69: HT/VHT have the same subtype placement, but independent +// presence conditions. These masks express subtype permission only. +enum ManagementElementPresence : unsigned int { HT_ELEMENT_NONE = 0, HT_CAPABILITIES_ALLOWED = 1, HT_OPERATION_ALLOWED = 2, EXTENDED_SUPPORTED_RATES_ALLOWED = 4, BASIC_HT_MCS_SET_PRESENT = 8, + VHT_CAPABILITIES_ALLOWED = 16, + VHT_OPERATION_ALLOWED = 32, }; -static void writeHtElements(MemoryOutputStream& stream, ElementWriter& elements, const Ptr& frame, unsigned int allowedElements) +static void writeManagementElements(MemoryOutputStream& stream, ElementWriter& elements, const Ptr& frame, unsigned int allowedElements) { if (!(allowedElements & EXTENDED_SUPPORTED_RATES_ALLOWED) && frame->getExtendedSupportedRatesPresent()) throw cRuntimeError("Extended Supported Rates element is not allowed in this management frame subtype"); @@ -347,6 +446,10 @@ static void writeHtElements(MemoryOutputStream& stream, ElementWriter& elements, throw cRuntimeError("HT Capabilities element is not allowed in this management frame subtype"); if (!(allowedElements & HT_OPERATION_ALLOWED) && frame->getHtOperationPresent()) throw cRuntimeError("HT Operation element is not allowed in this management frame subtype"); + if (!(allowedElements & VHT_CAPABILITIES_ALLOWED) && frame->getVhtCapabilitiesPresent()) + throw cRuntimeError("VHT Capabilities element is not allowed in this management frame subtype"); + if (!(allowedElements & VHT_OPERATION_ALLOWED) && frame->getVhtOperationPresent()) + throw cRuntimeError("VHT Operation element is not allowed in this management frame subtype"); if (frame->getHtCapabilitiesPresent()) { elements.beginModelledElement(); writeHtCapabilitiesElement(stream, frame->getHtCapabilities()); @@ -355,6 +458,14 @@ static void writeHtElements(MemoryOutputStream& stream, ElementWriter& elements, elements.beginModelledElement(); writeHtOperationElement(stream, frame->getHtOperation(), allowedElements & BASIC_HT_MCS_SET_PRESENT); } + if (frame->getVhtCapabilitiesPresent()) { + elements.beginModelledElement(); + writeVhtCapabilitiesElement(stream, frame->getVhtCapabilities()); + } + if (frame->getVhtOperationPresent()) { + elements.beginModelledElement(); + writeVhtOperationElement(stream, frame->getVhtOperation()); + } } static void readHtCapabilitiesElement(MemoryInputStream& stream, int length, const Ptr& frame) @@ -444,7 +555,7 @@ static void readHtOperationElement(MemoryInputStream& stream, int length, const frame->setHtOperation(operation); } -static void readHtElements(MemoryInputStream& stream, const Ptr& frame, unsigned int allowedElements, int modelledElementCount) +static void readManagementElements(MemoryInputStream& stream, const Ptr& frame, unsigned int allowedElements, int modelledElementCount) { while (stream.getRemainingLength() != b(0)) { if (stream.getRemainingLength() < B(2)) { @@ -520,6 +631,30 @@ static void readHtElements(MemoryInputStream& stream, const PtrmarkIncorrect(); + stream.seek(stream.getPosition() + declaredBodyLength); + } + else { + bool wasPresent = frame->getVhtCapabilitiesPresent(); + readVhtCapabilitiesElement(stream, length, frame); + if (!wasPresent && frame->getVhtCapabilitiesPresent()) + modelledElementCount++; + } + } + else if (elementId == VHT_OPERATION_ELEMENT_ID) { + if (!(allowedElements & VHT_OPERATION_ALLOWED)) { + frame->markIncorrect(); + stream.seek(stream.getPosition() + declaredBodyLength); + } + else { + bool wasPresent = frame->getVhtOperationPresent(); + readVhtOperationElement(stream, length, frame); + if (!wasPresent && frame->getVhtOperationPresent()) + modelledElementCount++; + } + } else readUnmodelledElement(stream, frame, elementId, length, modelledElementCount); } @@ -660,17 +795,17 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeUint16Le(authenticationFrame->getStatusCode()); // 4 Challenge text The challenge text information is present only in certain Authentication frames as defined in Table 7-17. // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. - writeHtElements(stream, elements, authenticationFrame, HT_ELEMENT_NONE); + writeManagementElements(stream, elements, authenticationFrame, HT_ELEMENT_NONE); } else if (auto deauthenticationFrame = dynamicPtrCast(chunk)) { // type = ST_DEAUTHENTICATION; stream.writeUint16Le(deauthenticationFrame->getReasonCode()); - writeHtElements(stream, elements, deauthenticationFrame, HT_ELEMENT_NONE); + writeManagementElements(stream, elements, deauthenticationFrame, HT_ELEMENT_NONE); } else if (auto disassociationFrame = dynamicPtrCast(chunk)) { // type = ST_DISASSOCIATION; stream.writeUint16Le(disassociationFrame->getReasonCode()); - writeHtElements(stream, elements, disassociationFrame, HT_ELEMENT_NONE); + writeManagementElements(stream, elements, disassociationFrame, HT_ELEMENT_NONE); } else if (auto probeRequestFrame = dynamicPtrCast(chunk)) { // type = ST_PROBEREQUEST; @@ -679,7 +814,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c writeSsidElement(stream, probeRequestFrame->getSSID()); // 2 Supported rates writeSupportedRateElements(stream, elements, probeRequestFrame); - writeHtElements(stream, elements, probeRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); + writeManagementElements(stream, elements, probeRequestFrame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); // 3 Request information May be included if dot11MultiDomainCapabilityEnabled is true. // 4 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. @@ -697,7 +832,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c writeSsidElement(stream, reassociationRequestFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, elements, reassociationRequestFrame); - writeHtElements(stream, elements, reassociationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); + writeManagementElements(stream, elements, reassociationRequestFrame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); // 6 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 7 Power Capability The Power Capability element shall be present if dot11SpectrumManagementRequired is true. // 8 Supported Channels The Supported Channels element shall be present if dot11SpectrumManagementRequired is true. @@ -716,7 +851,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c writeSsidElement(stream, associationRequestFrame->getSSID()); // 4 Supported rates writeSupportedRateElements(stream, elements, associationRequestFrame); - writeHtElements(stream, elements, associationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); + writeManagementElements(stream, elements, associationRequestFrame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); // 5 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 6 Power Capability The Power Capability element shall be present if dot11SpectrumManagementRequired is true. // 7 Supported Channel The Supported Channels element shall be present if dot11SpectrumManagementRequired is true. @@ -734,7 +869,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeUint16Le(encodeAssociationId(associationResponseFrame->getStatusCode(), associationResponseFrame->getAid())); // 4 Supported rates writeSupportedRateElements(stream, elements, associationResponseFrame); - writeHtElements(stream, elements, associationResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); + writeManagementElements(stream, elements, associationResponseFrame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); // 5 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 6 EDCA Parameter Set // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. @@ -749,7 +884,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeUint16Le(encodeAssociationId(reassociationResponseFrame->getStatusCode(), reassociationResponseFrame->getAid())); // 4 Supported rates writeSupportedRateElements(stream, elements, reassociationResponseFrame); - writeHtElements(stream, elements, reassociationResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); + writeManagementElements(stream, elements, reassociationResponseFrame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); // 5 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 6 EDCA Parameter Set // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. @@ -768,7 +903,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c writeSsidElement(stream, beaconFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, elements, beaconFrame); - writeHtElements(stream, elements, beaconFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); + writeManagementElements(stream, elements, beaconFrame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); // 6 Frequency-Hopping (FH) Parameter Set The FH Parameter Set information element is present within Beacon frames generated by STAs using FH PHYs. // 8 CF Parameter Set The CF Parameter Set information element is present only within Beacon frames generated by APs supporting a PCF. // 9 IBSS Parameter Set The IBSS Parameter Set information element is present only within Beacon frames generated by STAs in an IBSS. @@ -803,7 +938,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c writeSsidElement(stream, probeResponseFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, elements, probeResponseFrame); - writeHtElements(stream, elements, probeResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); + writeManagementElements(stream, elements, probeResponseFrame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); // 6 FH Parameter Set The FH Parameter Set information element is present within Probe Response frames generated by STAs using FH PHYs. // 8 CF Parameter Set The CF Parameter Set information element is present only within Probe Response frames generated by APs supporting a PCF. // 9 IBSS Parameter Set The IBSS Parameter Set information element is present only within Probe Response frames generated by STAs in an IBSS. @@ -837,19 +972,19 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre stream.readUint16Le(); frame->setSequenceNumber(stream.readUint16Le()); frame->setStatusCode((Ieee80211StatusCode)stream.readUint16Le()); - readHtElements(stream, frame, HT_ELEMENT_NONE, 0); + readManagementElements(stream, frame, HT_ELEMENT_NONE, 0); return frame; } else if (typeInfo == typeid(Ieee80211DeauthenticationFrame)) { auto frame = makeShared(); frame->setReasonCode((Ieee80211ReasonCode)stream.readUint16Le()); - readHtElements(stream, frame, HT_ELEMENT_NONE, 0); + readManagementElements(stream, frame, HT_ELEMENT_NONE, 0); return frame; } else if (typeInfo == typeid(Ieee80211DisassociationFrame)) { auto frame = makeShared(); frame->setReasonCode((Ieee80211ReasonCode)stream.readUint16Le()); - readHtElements(stream, frame, HT_ELEMENT_NONE, 0); + readManagementElements(stream, frame, HT_ELEMENT_NONE, 0); return frame; } else if (typeInfo == typeid(Ieee80211ProbeRequestFrame)) { @@ -860,7 +995,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); frame->setSupportedRates(supRat); - readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 2); + readManagementElements(stream, frame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 2); return frame; } else if (typeInfo == typeid(Ieee80211AssociationRequestFrame)) { @@ -873,7 +1008,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); frame->setSupportedRates(supRat); - readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 2); + readManagementElements(stream, frame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 2); return frame; } else if (typeInfo == typeid(Ieee80211ReassociationRequestFrame)) { @@ -888,7 +1023,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); frame->setSupportedRates(supRat); - readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 2); + readManagementElements(stream, frame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 2); return frame; } else if (typeInfo == typeid(Ieee80211AssociationResponseFrame)) { @@ -900,7 +1035,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); frame->setSupportedRates(supRat); - readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 1); + readManagementElements(stream, frame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 1); return frame; } else if (typeInfo == typeid(Ieee80211ReassociationResponseFrame)) { @@ -912,7 +1047,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); frame->setSupportedRates(supRat); - readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 1); + readManagementElements(stream, frame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED, 1); return frame; } else if (typeInfo == typeid(Ieee80211BeaconFrame)) { @@ -931,7 +1066,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); frame->setSupportedRates(supRat); - readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT, 2); + readManagementElements(stream, frame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT, 2); return frame; } else if (typeInfo == typeid(Ieee80211ProbeResponseFrame)) { @@ -950,7 +1085,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); frame->setSupportedRates(supRat); - readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT, 2); + readManagementElements(stream, frame, HT_CAPABILITIES_ALLOWED | VHT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | VHT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT, 2); return frame; } else diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index afa0c3221f5..7f9d4dc1704 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h" #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" @@ -365,7 +366,9 @@ void Ieee80211MgmtSta::startAssociation(ApInfo *ap, simtime_t timeout) body->setSSID(ap->ssid.c_str()); setSupportedRateElements(body); addHtCapabilities(body); - body->setChunkLength(B(2 + 2 + (2 + strlen(body->getSSID()))) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); + addVhtCapabilities(body); + body->setChunkLength(B(2 + 2 + (2 + strlen(body->getSSID()))) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body) + getVhtMgmtElementsLength(body)); + pendingVhtGeneration = mib->getVhtCapabilityGeneration(); sendManagementFrame("Assoc", body, ST_ASSOCIATIONREQUEST, ap->address); reassociationInProgress = false; @@ -388,7 +391,9 @@ void Ieee80211MgmtSta::startReassociation(ApInfo *ap, simtime_t timeout) body->setSSID(ap->ssid.c_str()); setSupportedRateElements(body); addHtCapabilities(body); - body->setChunkLength(B(2 + 2 + 6 + (2 + strlen(body->getSSID()))) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); + addVhtCapabilities(body); + body->setChunkLength(B(2 + 2 + 6 + (2 + strlen(body->getSSID()))) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body) + getVhtMgmtElementsLength(body)); + pendingVhtGeneration = mib->getVhtCapabilityGeneration(); sendManagementFrame("Reassoc", body, ST_REASSOCIATIONREQUEST, ap->address); reassociationInProgress = true; assocTimeoutMsg = new cMessage("assocTimeout", MK_ASSOC_TIMEOUT); @@ -487,7 +492,8 @@ void Ieee80211MgmtSta::sendProbeRequest() body->setSSID(scanning.ssid.c_str()); setSupportedRateElements(body); addHtCapabilities(body); - body->setChunkLength(B(2 + scanning.ssid.length()) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body)); + addVhtCapabilities(body); + body->setChunkLength(B(2 + scanning.ssid.length()) + getSupportedRateElementsLength(body) + getHtMgmtElementsLength(body) + getVhtMgmtElementsLength(body)); sendManagementFrame("ProbeReq", body, ST_PROBEREQUEST, scanning.bssid); } @@ -622,7 +628,7 @@ void Ieee80211MgmtSta::clearCurrentAssociation() { ASSERT(mib->getBssStationData().isAssociated); mib->setAssociated(false); - mib->removePeerHtCapabilities(assocAP.address); + mib->removePeerCapabilities(assocAP.address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; assocAP = AssociatedApInfo(); // clear it @@ -827,7 +833,7 @@ void Ieee80211MgmtSta::handleDeauthenticationFrame(Packet *packet, const PtrauthTimeoutMsg); pendingAp->authTimeoutMsg = nullptr; } - mib->removePeerHtCapabilities(address); + mib->removePeerCapabilities(address); cancelPendingAssociation(); if (pendingReassociation) sendReassociationConfirm(pendingAp, PRC_REFUSED); @@ -852,7 +858,7 @@ void Ieee80211MgmtSta::handleDeauthenticationFrame(Packet *packet, const PtrisAuthenticated = false; - mib->removePeerHtCapabilities(address); + mib->removePeerCapabilities(address); delete packet; } @@ -898,6 +904,15 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrpeekData(); int statusCode = responseBody->getStatusCode(); + Ieee80211VhtCapabilities responseVhtCapabilities; + Ieee80211VhtOperation responseVhtOperation; + bool responseVhtValid = mib->isVhtOperationSupported() && pendingVhtGeneration == mib->getVhtCapabilityGeneration() && + ap->vhtAdvertisementValid && + decodeVhtCapabilities(responseBody, responseVhtCapabilities) && + decodeVhtOperation(responseBody, responseVhtOperation) && + supportsBasicVhtMcsSet(mib->getLocalVhtCapabilities(), responseVhtOperation) && + supportsBasicVhtMcsSet(responseVhtCapabilities, responseVhtOperation); + HtAssociationResponseStatus responseHtStatus = HtAssociationResponseStatus::LEGACY; Ieee80211HtCapabilities responseHtCapabilities; Ieee80211HtOperation responseHtOperation; @@ -925,7 +940,7 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrgetBssStationData().isAssociated || assocAP.address != ap->address) - mib->removePeerHtCapabilities(ap->address); + mib->removePeerCapabilities(ap->address); } else { EV << "Association successful, AP address=" << ap->address << "\n"; @@ -933,7 +948,7 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrgetBssStationData().isAssociated) { EV << "Breaking existing association with AP address=" << assocAP.address << "\n"; mib->setAssociated(false); - mib->removePeerHtCapabilities(assocAP.address); + mib->removePeerCapabilities(assocAP.address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; assocAP = AssociatedApInfo(); @@ -950,7 +965,7 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrsetPeerHtCapabilities(ap->address, responseHtCapabilities); else { - mib->removePeerHtCapabilities(ap->address); + mib->removePeerCapabilities(ap->address); if (responseHtStatus == HtAssociationResponseStatus::INVALID_HT) { EV_WARN << "Association succeeded without usable HT negotiation with AP address=" << ap->address << ": " << responseHtReason << "\n"; @@ -963,6 +978,11 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrsetPeerVhtCapabilities(ap->address, responseVhtCapabilities, responseVhtOperation); + else + mib->removePeerVhtCapabilities(ap->address); + mib->publishStateChange(); emit(l2AssociatedSignal, myIface, ap); } @@ -1064,7 +1084,7 @@ void Ieee80211MgmtSta::handleReassociationFailure(ApInfo *ap) if (shouldDisassociateOnReassociationFailure(mib->getBssStationData().isAssociated, assocAP.address, ap->address)) disassociate(); else { - mib->removePeerHtCapabilities(ap->address); + mib->removePeerCapabilities(ap->address); if (mib->getBssStationData().isAssociated) changeChannel(assocAP.channel); } @@ -1225,6 +1245,11 @@ bool Ieee80211MgmtSta::storeAPInfo(Packet *packet, const PtrisVhtOperationSupported() && + decodeVhtCapabilities(body, candidate.vhtCapabilities) && + decodeVhtOperation(body, candidate.vhtOperation) && + supportsBasicVhtMcsSet(mib->getLocalVhtCapabilities(), candidate.vhtOperation) && + supportsBasicVhtMcsSet(candidate.vhtCapabilities, candidate.vhtOperation); candidate.beaconInterval = body->getBeaconInterval(); auto signalPowerInd = packet->getTag(); bool currentAp = address == assocAP.address; @@ -1248,6 +1273,9 @@ bool Ieee80211MgmtSta::storeAPInfo(Packet *packet, const PtrextendedSupportedRates = candidate.extendedSupportedRates; ap->htCapabilitiesPresent = candidate.htCapabilitiesPresent; ap->htCapabilities = candidate.htCapabilities; + ap->vhtAdvertisementValid = candidate.vhtAdvertisementValid; + ap->vhtCapabilities = candidate.vhtCapabilities; + ap->vhtOperation = candidate.vhtOperation; ap->htOperationPresent = candidate.htOperationPresent; ap->htOperation = candidate.htOperation; ap->beaconInterval = candidate.beaconInterval; @@ -1269,8 +1297,12 @@ bool Ieee80211MgmtSta::storeAPInfo(Packet *packet, const PtrremovePeerHtCapabilities(address); + mib->removePeerCapabilities(address); } + if (candidate.vhtAdvertisementValid) + mib->setPeerVhtCapabilities(address, candidate.vhtCapabilities, candidate.vhtOperation); + else + mib->removePeerVhtCapabilities(address); } else if (signalPowerInd != nullptr && currentAp) assocAP.rxPower = candidate.rxPower; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h index a2c4c6ce47a..39090accd92 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h @@ -80,6 +80,9 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase Ieee80211ExtendedSupportedRatesElement extendedSupportedRates; bool htCapabilitiesPresent = false; Ieee80211HtCapabilities htCapabilities; + bool vhtAdvertisementValid = false; + Ieee80211VhtCapabilities vhtCapabilities; + Ieee80211VhtOperation vhtOperation; bool htOperationPresent = false; Ieee80211HtOperation htOperation; simtime_t beaconInterval; @@ -126,6 +129,7 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase AccessPointList apList; // associated Access Point + uint64_t pendingVhtGeneration = 0; cMessage *assocTimeoutMsg; // if non-nullptr: association is in progress bool reassociationInProgress = false; AssociatedApInfo assocAP; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h new file mode 100644 index 00000000000..8b6d1a95fe7 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h @@ -0,0 +1,106 @@ +// Copyright (C) 2026 INET Framework contributors +// SPDX-License-Identifier: LGPL-3.0-or-later +#ifndef __INET_IEEE80211VHTMGMTELEMENTS_H +#define __INET_IEEE80211VHTMGMTELEMENTS_H +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211VhtCapabilities.h" + +namespace inet { +namespace ieee80211 { + +inline bool decodeVhtCapabilities(const Ptr& frame, Ieee80211VhtCapabilities& capabilities) +{ + capabilities = Ieee80211VhtCapabilities(); + if (!frame->getVhtCapabilitiesPresent() || frame->isIncorrect() || frame->isIncomplete()) + return false; + const auto& element = frame->getVhtCapabilities(); + // 80+80 support is not modeled. Value 2 also supports contiguous 160 MHz. + if (element.supportedChannelWidthSet < 0 || element.supportedChannelWidthSet > 2) + return false; + capabilities.supported160Mhz = element.supportedChannelWidthSet != 0; + capabilities.shortGi80 = element.shortGi80; + capabilities.shortGi160 = element.shortGi160; + capabilities.rxHighestLongGiRateMbps = element.rxHighestLongGiRateMbps; + capabilities.txHighestLongGiRateMbps = element.txHighestLongGiRateMbps; + if (capabilities.rxHighestLongGiRateMbps < 0 || capabilities.rxHighestLongGiRateMbps > 8191 || + capabilities.txHighestLongGiRateMbps < 0 || capabilities.txHighestLongGiRateMbps > 8191) + return false; + for (int i = 0; i < 8; i++) { + capabilities.rxMaxMcs[i] = element.rxMaxMcs[i]; + capabilities.txMaxMcs[i] = element.txMaxMcs[i]; + } + // VHT SGI at 20/40 MHz is conveyed by HT Capabilities, not the VHT IE. + if (frame->getHtCapabilitiesPresent()) { + capabilities.shortGi20 = frame->getHtCapabilities().shortGi20; + capabilities.shortGi40 = frame->getHtCapabilities().shortGi40; + } + return isValidVhtMcsMap(capabilities.rxMaxMcs) && isValidVhtMcsMap(capabilities.txMaxMcs); +} + +inline bool decodeVhtOperation(const Ptr& frame, Ieee80211VhtOperation& operation) +{ + operation = Ieee80211VhtOperation(); + if (!frame->getVhtOperationPresent() || frame->isIncorrect() || frame->isIncomplete()) + return false; + const auto& element = frame->getVhtOperation(); + if (element.centerFrequencySegment0 < 0 || element.centerFrequencySegment0 > 255 || + element.centerFrequencySegment1 < 0 || element.centerFrequencySegment1 > 255) + return false; + operation.centerFrequencySegment0 = element.centerFrequencySegment0; + operation.centerFrequencySegment1 = element.centerFrequencySegment1; + // IEEE Std 802.11-2024, 9.4.2.157: revised signaling uses width 1 for + // contiguous 160 MHz with segment centers separated by eight channels. + if (element.channelWidth == 0) + operation.channelWidth = frame->getHtOperationPresent() && frame->getHtOperation().staChannelWidth40Mhz ? MHz(40) : MHz(20); + else if (element.channelWidth == 1 && element.centerFrequencySegment0 != 0 && element.centerFrequencySegment1 == 0) + operation.channelWidth = MHz(80); + else if ((element.channelWidth == 2 && element.centerFrequencySegment0 != 0) || + (element.channelWidth == 1 && element.centerFrequencySegment0 != 0 && element.centerFrequencySegment1 != 0 && std::abs(element.centerFrequencySegment0 - element.centerFrequencySegment1) == 8)) + operation.channelWidth = MHz(160); + else + return false; // Unsupported 80+80 or invalid width/center encoding. + for (int i = 0; i < 8; i++) { + int value = element.basicMaxMcs[i]; + if (value != -1 && value != 7 && value != 8 && value != 9) + return false; + operation.basicMaxMcs[i] = value; + } + return true; +} + +inline void setVhtCapabilities(const Ptr& frame, const Ieee80211VhtCapabilities& capabilities) +{ + Ieee80211VhtCapabilitiesElement element; + element.supportedChannelWidthSet = capabilities.supported160Mhz ? 1 : 0; + element.shortGi80 = capabilities.shortGi80; + element.shortGi160 = capabilities.shortGi160; + element.rxHighestLongGiRateMbps = capabilities.rxHighestLongGiRateMbps; + element.txHighestLongGiRateMbps = capabilities.txHighestLongGiRateMbps; + for (int i = 0; i < 8; i++) { + element.rxMaxMcs[i] = capabilities.rxMaxMcs[i]; + element.txMaxMcs[i] = capabilities.txMaxMcs[i]; + } + frame->setVhtCapabilities(element); + frame->setVhtCapabilitiesPresent(true); +} + +inline void setVhtOperation(const Ptr& frame, const Ieee80211VhtOperation& operation) +{ + Ieee80211VhtOperationElement element; + element.channelWidth = operation.channelWidth <= MHz(40) ? 0 : 1; + element.centerFrequencySegment0 = operation.centerFrequencySegment0; + element.centerFrequencySegment1 = operation.centerFrequencySegment1; + for (int i = 0; i < 8; i++) + element.basicMaxMcs[i] = operation.basicMaxMcs[i]; + frame->setVhtOperation(element); + frame->setVhtOperationPresent(true); +} + +inline B getVhtMgmtElementsLength(const Ptr& frame) +{ + return B((frame->getVhtCapabilitiesPresent() ? 14 : 0) + (frame->getVhtOperationPresent() ? 7 : 0)); +} + +} // namespace ieee80211 +} // namespace inet +#endif diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 0a68b50c469..43accb52a97 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -77,6 +77,8 @@ void Ieee80211Mib::commitBss(const std::string& ssid, const MacAddress& bssid, c void Ieee80211Mib::clearBss() { checkStateMutation(); + stateChangePending |= !peerVhtStates.empty(); + peerVhtStates.clear(); stateChangePending |= bssActive || !peerHtStates.empty() || !bssAccessPointData.stations.empty() || !bssAccessPointData.associationIds.empty(); bssActive = false; @@ -242,6 +244,60 @@ void Ieee80211Mib::clearPeerHtCapabilities() peerHtStates.clear(); } +void Ieee80211Mib::installLocalVhtCapabilities(const Ieee80211VhtCapabilities& capabilities, bool supported) +{ + checkStateMutation(); + if (localVhtCapabilitiesValid == supported && localVhtCapabilities == capabilities) + return; + localVhtCapabilities = capabilities; + localVhtCapabilitiesValid = supported; + ++vhtCapabilityGeneration; + // Pending response snapshots are invalidated by the generation change. + // A newly configured local VHT profile requires fresh peer negotiation. + peerVhtStates.clear(); + stateChangePending = true; +} + +const Ieee80211Mib::PeerVhtState *Ieee80211Mib::findPeerVhtState(const MacAddress& address) const +{ + auto it = peerVhtStates.find(address); + return it == peerVhtStates.end() ? nullptr : &it->second; +} + +void Ieee80211Mib::setPeerVhtCapabilities(const MacAddress& address, const Ieee80211VhtCapabilities& capabilities, const Ieee80211VhtOperation& operation) +{ + checkStateMutation(); + if (!localVhtCapabilitiesValid || !isValidVhtMcsMap(capabilities.rxMaxMcs) || !isValidVhtMcsMap(capabilities.txMaxMcs) || + !supportsBasicVhtMcsSet(localVhtCapabilities, operation) || !supportsBasicVhtMcsSet(capabilities, operation)) { + removePeerVhtCapabilities(address); + return; + } + auto it = peerVhtStates.find(address); + if (it != peerVhtStates.end() && it->second.advertisedCapabilities == capabilities && it->second.operation == operation) + return; + peerVhtStates[address] = {capabilities, operation}; + stateChangePending = true; +} + +void Ieee80211Mib::removePeerVhtCapabilities(const MacAddress& address) +{ + checkStateMutation(); + stateChangePending |= peerVhtStates.erase(address) != 0; +} + +void Ieee80211Mib::removePeerCapabilities(const MacAddress& address) +{ + removePeerHtCapabilities(address); + removePeerVhtCapabilities(address); +} + +void Ieee80211Mib::clearPeerCapabilities() +{ + clearPeerHtCapabilities(); + stateChangePending |= !peerVhtStates.empty(); + peerVhtStates.clear(); +} + std::string Ieee80211Mib::getSsidStr() const { if (mode == INFRASTRUCTURE) @@ -333,7 +389,7 @@ void Ieee80211Mib::releaseAssociationId(const MacAddress& address) checkStateMutation(); associationIdReservations.erase(address); stateChangePending |= bssAccessPointData.associationIds.erase(address) != 0; - removePeerHtCapabilities(address); + removePeerCapabilities(address); } void Ieee80211Mib::clearAssociationIds() @@ -343,7 +399,7 @@ void Ieee80211Mib::clearAssociationIds() bssAccessPointData.stations.clear(); associationIdReservations.clear(); bssAccessPointData.associationIds.clear(); - clearPeerHtCapabilities(); + clearPeerCapabilities(); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index e048dbf9682..549e807e97b 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -13,6 +13,7 @@ #include "inet/common/SimpleModule.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211VhtCapabilities.h" namespace inet { @@ -70,6 +71,11 @@ class INET_API Ieee80211Mib : public SimpleModule std::shared_ptr negotiatedCapabilities; }; + struct PeerVhtState { + Ieee80211VhtCapabilities advertisedCapabilities; + Ieee80211VhtOperation operation; + }; + public: MacAddress address; Mode mode = static_cast(-1); @@ -83,6 +89,10 @@ class INET_API Ieee80211Mib : public SimpleModule // This is a deliberately model-backed subset, not a full Annex C HT MIB implementation. bool localHtCapabilitiesValid = false; Ieee80211HtCapabilities localHtCapabilities; + uint64_t vhtCapabilityGeneration = 0; + bool localVhtCapabilitiesValid = false; + Ieee80211VhtCapabilities localVhtCapabilities; + Ieee80211VhtOperation localVhtOperation; private: Ieee80211HtOperation htOperation; @@ -97,6 +107,7 @@ class INET_API Ieee80211Mib : public SimpleModule void checkStateMutation() const; std::map associationIdReservations; std::map peerHtStates; + std::map peerVhtStates; protected: virtual void initialize(int stage) override; @@ -144,6 +155,16 @@ class INET_API Ieee80211Mib : public SimpleModule void setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities); void removePeerHtCapabilities(const MacAddress& address); void clearPeerHtCapabilities(); + void installLocalVhtCapabilities(const Ieee80211VhtCapabilities& capabilities, bool supported); + const Ieee80211VhtCapabilities& getLocalVhtCapabilities() const { return localVhtCapabilities; } + const Ieee80211VhtOperation& getLocalVhtOperation() const { return localVhtOperation; } + uint64_t getVhtCapabilityGeneration() const { return vhtCapabilityGeneration; } + bool isVhtOperationSupported() const { return localVhtCapabilitiesValid; } + const PeerVhtState *findPeerVhtState(const MacAddress& address) const; + void setPeerVhtCapabilities(const MacAddress& address, const Ieee80211VhtCapabilities& capabilities, const Ieee80211VhtOperation& operation); + void removePeerVhtCapabilities(const MacAddress& address); + void removePeerCapabilities(const MacAddress& address); + void clearPeerCapabilities(); }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index ac929589c40..35b54143ac6 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -19,6 +19,10 @@ import inet.common.SimpleModule; simple Ieee80211Mib extends SimpleModule { parameters: + bool vhtSupported = default(true); // Effective only for a VHT mode set + object vhtRxMcsMap = default([9, 9, 9, 9, 9, 9, 9, 9]); // Per-NSS maximum MCS: -1, 7, 8 or 9; limited by PHY antennas and the primary-20 catalog + object vhtTxMcsMap = default([9, 9, 9, 9, 9, 9, 9, 9]); // Per-NSS maximum MCS: -1, 7, 8 or 9; limited by PHY antennas and the primary-20 catalog + @class(Ieee80211Mib); @signal[bssStateChanged](type=bool); // committed active state; synchronous read-only observation // Model-backed subset of IEEE Std 802.11-2024 HT capability/operation state; not a full Annex C MIB. diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211VhtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211VhtCapabilities.h new file mode 100644 index 00000000000..00da36591db --- /dev/null +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211VhtCapabilities.h @@ -0,0 +1,65 @@ +// Copyright (C) 2026 INET Framework contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_IEEE80211VHTCAPABILITIES_H +#define __INET_IEEE80211VHTCAPABILITIES_H + +#include +#include "inet/common/Units.h" + +namespace inet { +namespace ieee80211 { + +// IEEE Std 802.11-2024, 9.4.2.156.3: each map entry describes an inclusive +// MCS maximum (7, 8, 9), or -1 for an unsupported spatial-stream count. +struct INET_API Ieee80211VhtCapabilities +{ + std::array rxMaxMcs = {{-1, -1, -1, -1, -1, -1, -1, -1}}; + std::array txMaxMcs = {{-1, -1, -1, -1, -1, -1, -1, -1}}; + bool supported160Mhz = false; + bool shortGi20 = false; + bool shortGi40 = false; + bool shortGi80 = false; + bool shortGi160 = false; + int rxHighestLongGiRateMbps = 0; + int txHighestLongGiRateMbps = 0; + bool operator==(const Ieee80211VhtCapabilities& other) const { + return rxMaxMcs == other.rxMaxMcs && txMaxMcs == other.txMaxMcs && supported160Mhz == other.supported160Mhz && + shortGi20 == other.shortGi20 && shortGi40 == other.shortGi40 && shortGi80 == other.shortGi80 && + shortGi160 == other.shortGi160 && rxHighestLongGiRateMbps == other.rxHighestLongGiRateMbps && + txHighestLongGiRateMbps == other.txHighestLongGiRateMbps; + } +}; + +struct INET_API Ieee80211VhtOperation +{ + Hz channelWidth = MHz(20); + int centerFrequencySegment0 = 0; + int centerFrequencySegment1 = 0; + std::array basicMaxMcs = {{7, -1, -1, -1, -1, -1, -1, -1}}; + bool operator==(const Ieee80211VhtOperation& other) const { + return channelWidth == other.channelWidth && centerFrequencySegment0 == other.centerFrequencySegment0 && + centerFrequencySegment1 == other.centerFrequencySegment1 && basicMaxMcs == other.basicMaxMcs; + } +}; + +inline bool isValidVhtMcsMap(const std::array& map) +{ + for (int maximum : map) + if (maximum != -1 && maximum != 7 && maximum != 8 && maximum != 9) + return false; + return map[0] >= 7; +} + +inline bool supportsBasicVhtMcsSet(const Ieee80211VhtCapabilities& capabilities, const Ieee80211VhtOperation& operation) +{ + for (size_t nss = 0; nss < operation.basicMaxMcs.size(); nss++) + if (operation.basicMaxMcs[nss] >= 0 && + (capabilities.rxMaxMcs[nss] < operation.basicMaxMcs[nss] || capabilities.txMaxMcs[nss] < operation.basicMaxMcs[nss])) + return false; + return true; +} + +} // namespace ieee80211 +} // namespace inet +#endif diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 387c0c467ac..03ffa40c1b7 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -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, 8180-0d11/tplx;a5d5-2820/~tNl, PASS, Ipv4 +/examples/wireless/lan80211ac/, -f omnetpp.ini -c Ping1 -r 0, 100s, e382-0a32/tplx;45db-b12b/~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;66de-a722/~tND;7a22-c289/tyf, PASS, wireless Ipv4 diff --git a/tests/fingerprint/store.json b/tests/fingerprint/store.json index 6b257bb7e61..072c6784b34 100644 --- a/tests/fingerprint/store.json +++ b/tests/fingerprint/store.json @@ -35623,7 +35623,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "8180-0d11", + "fingerprint": "e382-0a32", "timestamp": 1681992800.58698, "itervars": "$repetition==0" }, @@ -35635,7 +35635,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "a5d5-2820", + "fingerprint": "45db-b12b", "timestamp": 1681992800.5873275, "itervars": "$repetition==0" }, diff --git a/tests/module/Ieee80211VhtAssociation_1.test b/tests/module/Ieee80211VhtAssociation_1.test new file mode 100644 index 00000000000..d3af71d2ea5 --- /dev/null +++ b/tests/module/Ieee80211VhtAssociation_1.test @@ -0,0 +1,323 @@ +%description: +Detailed AP/STA VHT association and reassociation with two asymmetric peers. +Exercise DCF and HCF, serialized IEs, negotiated data transmissions, absent-IE +fallback, disassociation isolation and coordinated mode-set invalidation. + +%file: VhtAssociation.cc +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +// Request the catalog maximum so peer filtering must narrow width/NSS/MCS/GI. +class VhtProbeRateControl : public AarfRateControl +{ + protected: + virtual const IIeee80211Mode *getInitialMode() override { return modeSet->getFastestMode(); } +}; +Define_Module(VhtProbeRateControl); + +class VhtProbeMac : public Ieee80211Mac +{ + public: + int delivered = 0; + int vhtSent = 0; + int legacySent = 0; + int requests = 0; + int responses = 0; + int reRequests = 0; + int reResponses = 0; + virtual void sendDownFrame(Packet *packet) override + { + auto header = packet->peekAtFront(); + if (dynamicPtrCast(header)) { + auto mode = packet->getTag()->getMode(); + auto peer = mib->findPeerVhtState(header->getReceiverAddress()); + if (peer == nullptr) { + ASSERT(mode->getVhtMcsIndex() < 0); + legacySent++; + } + else { + ASSERT(mode->getVhtMcsIndex() >= 0); + auto data = mode->getDataMode(); + int nss = data->getNumberOfSpatialStreams(); + ASSERT(mode->getVhtMcsIndex() <= peer->advertisedCapabilities.rxMaxMcs[nss - 1]); + ASSERT(mode->getVhtMcsIndex() <= mib->getLocalVhtCapabilities().txMaxMcs[nss - 1]); + ASSERT(data->getBandwidth() == MHz(20)); + ASSERT(data->getGuardInterval() == SimTime(800, SIMTIME_NS)); + vhtSent++; + } + } + else if (header->getType() == ST_ASSOCIATIONREQUEST || header->getType() == ST_REASSOCIATIONREQUEST || + header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE) { + auto body = packet->peekAt(header->getChunkLength()); + bool response = header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE; + ASSERT(body->getVhtCapabilitiesPresent() == mib->isVhtOperationSupported()); + ASSERT(body->getVhtOperationPresent() == (response && mib->isVhtOperationSupported())); + Packet wire("actual-management-body", body); + auto bytes = wire.peekAllAsBytes(); + Packet encoded("encoded", bytes); + Ptr decoded; + if (header->getType() == ST_ASSOCIATIONREQUEST) { + decoded = encoded.peekAtFront(); + requests++; + } + else if (header->getType() == ST_REASSOCIATIONREQUEST) { + decoded = encoded.peekAtFront(); + reRequests++; + } + else if (header->getType() == ST_ASSOCIATIONRESPONSE) { + decoded = encoded.peekAtFront(); + responses++; + } + else { + decoded = encoded.peekAtFront(); + reResponses++; + } + ASSERT(!decoded->isIncorrect() && !decoded->isIncomplete()); + ASSERT(decoded->getVhtCapabilitiesPresent() == body->getVhtCapabilitiesPresent()); + if (body->getVhtCapabilitiesPresent()) + for (int i = 0; i < 8; i++) { + ASSERT(decoded->getVhtCapabilities().rxMaxMcs[i] == body->getVhtCapabilities().rxMaxMcs[i]); + ASSERT(decoded->getVhtCapabilities().txMaxMcs[i] == body->getVhtCapabilities().txMaxMcs[i]); + } + } + Ieee80211Mac::sendDownFrame(packet); + } + virtual void sendUpFrame(Packet *packet) override + { + if (dynamicPtrCast(packet->peekAtFront())) { + Enter_Method("sendUpFrame"); + take(packet); + delivered++; + delete packet; // End synthetic data at the MAC service boundary. + } + else + Ieee80211Mac::sendUpFrame(packet); + } +}; +Define_Module(VhtProbeMac); + +// Inherited AP frame builders must dispatch through the advertisement hooks. +class VhtProbeAp : public Ieee80211MgmtAp +{ + public: + mutable int capabilitiesAdded = 0; + mutable int operationAdded = 0; + protected: + virtual void addVhtCapabilities(const Ptr& frame) const override + { + capabilitiesAdded++; + Ieee80211MgmtAp::addVhtCapabilities(frame); + } + virtual void addVhtOperation(const Ptr& frame) const override + { + operationAdded++; + Ieee80211MgmtAp::addVhtOperation(frame); + } +}; +Define_Module(VhtProbeAp); + +class VhtProbeSta : public Ieee80211MgmtSta +{ + public: + void reassociate(const MacAddress& ap) + { + Enter_Method("reassociate"); + Ieee80211Prim_ReassociateRequest request; + request.setAddress(ap); + request.setTimeout(SimTime(20, SIMTIME_MS)); + processReassociateCommand(&request); + } + void disconnect(const MacAddress& ap) + { + Enter_Method("disconnect"); + Ieee80211Prim_DisassociateRequest request; + request.setAddress(ap); + request.setReasonCode(RC_DIASS_MS_LEAVING); + processDisassociateCommand(&request); + } +}; +Define_Module(VhtProbeSta); + +class VhtAssociationTest : public cSimpleModule +{ + cMessage *action = nullptr; + int phase = 0; + Ieee80211Mib *mib(const char *host) { return check_and_cast(getModuleByPath((std::string("^.") + host + ".wlan[0].mib").c_str())); } + VhtProbeMac *mac(const char *host) { return check_and_cast(getModuleByPath((std::string("^.") + host + ".wlan[0].mac").c_str())); } + void sendData(const char *from, const char *to) + { + auto sender = mac(from); + auto receiver = mac(to); + auto packet = new Packet("negotiated-vht-data"); + auto header = makeShared(); + header->setType(par("qos") ? ST_DATA_WITH_QOS : ST_DATA); + header->setChunkLength(DATAFRAME_HEADER_MINLENGTH + (par("qos") ? QOSCONTROL_PART_LENGTH : b(0))); + header->setTid(0); + header->setReceiverAddress(receiver->getAddress()); + header->setTransmitterAddress(sender->getAddress()); + header->setAddress3(receiver->getAddress()); + packet->insertAtBack(header); + packet->insertAtBack(makeShared(B(100))); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(trailer); + sender->processUpperFrame(packet, header); + } + protected: + virtual void initialize() override { action = new cMessage("phase"); scheduleAt(SimTime(30, SIMTIME_MS), action); } + virtual void handleMessage(cMessage *) override + { + auto ap = mib("ap"); + auto first = mib("sta[0]"); + auto second = mib("sta[1]"); + bool missing = par("missing"); + if (phase == 0) { + ASSERT(first->getBssStationData().isAssociated && second->getBssStationData().isAssociated); + ASSERT(ap->findPeerVhtState(first->address)->advertisedCapabilities.rxMaxMcs[0] == 7); + ASSERT((ap->findPeerVhtState(second->address) == nullptr) == missing); + ASSERT((second->findPeerVhtState(ap->address) == nullptr) == missing); + if (!missing) { + ASSERT(ap->findPeerVhtState(second->address)->advertisedCapabilities.rxMaxMcs[0] == 8); + ASSERT(ap->findPeerVhtState(second->address)->advertisedCapabilities.rxMaxMcs[1] == 8); + } + sendData("ap", "sta[0]"); + sendData("ap", "sta[1]"); + sendData("sta[0]", "ap"); + sendData("sta[1]", "ap"); + } + else if (phase == 1) { + ASSERT(mac("ap")->delivered == 2 && mac("sta[0]")->delivered == 1 && mac("sta[1]")->delivered == 1); + ASSERT(mac("sta[0]")->vhtSent >= 1); + ASSERT(missing ? mac("sta[1]")->legacySent >= 1 : mac("sta[1]")->vhtSent >= 1); + // Change only STA0's receiver limit before a real same-AP reassociation. + auto capabilities = first->getLocalVhtCapabilities(); + capabilities.rxMaxMcs[0] = 8; + first->installLocalVhtCapabilities(capabilities, true); + check_and_cast(getModuleByPath("^.sta[0].wlan[0].mgmt"))->reassociate(ap->address); + } + else if (phase == 2) { + ASSERT(first->getBssStationData().isAssociated); + ASSERT(ap->findPeerVhtState(first->address)->advertisedCapabilities.rxMaxMcs[0] == 8); + ASSERT((ap->findPeerVhtState(second->address) == nullptr) == missing); + ASSERT(mac("sta[0]")->reRequests > 0 && mac("ap")->reResponses > 0); + sendData("ap", "sta[0]"); + } + else if (phase == 3) { + ASSERT(mac("sta[0]")->delivered == 2); + check_and_cast(getModuleByPath("^.sta[0].wlan[0].mgmt"))->disconnect(ap->address); + ASSERT(first->findPeerVhtState(ap->address) == nullptr); + } + else if (phase == 4) { + ASSERT(ap->findPeerVhtState(first->address) == nullptr); + ASSERT((ap->findPeerVhtState(second->address) == nullptr) == missing); + auto radio = check_and_cast(getModuleByPath("^.ap.wlan[0].radio")); + ASSERT(radio->getTransmissionState() != IRadio::TRANSMISSION_STATE_TRANSMITTING); + radio->setMode(Ieee80211ModeSet::getModeSet("ac")->getMode(Mbps(24))); + radio->setModeSet(Ieee80211ModeSet::getModeSet("a")); + radio->setModeSet(Ieee80211ModeSet::getModeSet("ac")); + ASSERT(ap->findPeerVhtState(second->address) == nullptr); + sendData("ap", "sta[1]"); + } + else { + ASSERT(mac("sta[1]")->delivered == 2); + ASSERT(mac("ap")->legacySent >= 1); + ASSERT(mac("ap")->responses >= 2 && mac("sta[0]")->requests >= 1 && mac("sta[1]")->requests >= 1); + auto mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + ASSERT(mgmt->capabilitiesAdded >= mac("ap")->responses); + ASSERT(mgmt->operationAdded >= mac("ap")->responses); + std::cout << "VHT exchange verified: qos=" << par("qos").boolValue() << " missing=" << missing << "\n"; + endSimulation(); + return; + } + phase++; + scheduleAfter(SimTime(10, SIMTIME_MS), action); + } + virtual void finish() override { ASSERT(phase == 5); } + public: + virtual ~VhtAssociationTest() { cancelAndDelete(action); } +}; +Define_Module(VhtAssociationTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.linklayer.ieee80211.mac.ratecontrol.AarfRateControl; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtSta; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple VhtProbeRateControl extends AarfRateControl { parameters: @class(::VhtProbeRateControl); } +module VhtProbeMac extends Ieee80211Mac { parameters: @class(::VhtProbeMac); } +simple VhtProbeAp extends Ieee80211MgmtAp { parameters: @class(::VhtProbeAp); } +simple VhtProbeSta extends Ieee80211MgmtSta { parameters: @class(::VhtProbeSta); } +simple VhtAssociationTest { parameters: @class(::VhtAssociationTest); bool qos; bool missing; } +network VhtNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + sta[2]: WirelessHost; + test: VhtAssociationTest; +} + +%inifile: omnetpp.ini +[General] +network = VhtNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 100ms +seed-set = 0 +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" # Deterministic simultaneous data submissions exercise contention. +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.test.qos = ${qos=false,true} +*.test.missing = ${missing=false,true} +**.mobility.initFromDisplayString = false +*.ap.mobility.initialX = 10m +*.sta[0].mobility.initialX = 11m +*.sta[1].mobility.initialX = 12m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "ac" +**.wlan[*].bitrate = -1bps +**.wlan[*].radio.bandName = "5 GHz (20 MHz)" +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].radio.antenna.numAntennas = 2 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].mac.typename = "VhtProbeMac" +**.mac.qosStation = ${qos} +**.mac.*.rateControl.typename = "VhtProbeRateControl" +*.sta[*].wlan[*].mgmt.typename = "VhtProbeSta" +*.sta[*].wlan[*].agent.typename = "Ieee80211AgentSta" +*.ap.wlan[*].mgmt.typename = "VhtProbeAp" +*.ap.wlan[*].mgmt.beaconInterval = 1s +*.sta[0].wlan[*].mib.vhtRxMcsMap = [7,-1,-1,-1,-1,-1,-1,-1] +*.sta[0].wlan[*].mib.vhtTxMcsMap = [8,8,-1,-1,-1,-1,-1,-1] +*.sta[1].wlan[*].mib.vhtRxMcsMap = [8,8,-1,-1,-1,-1,-1,-1] +*.sta[1].wlan[*].mib.vhtTxMcsMap = [7,-1,-1,-1,-1,-1,-1,-1] +*.sta[1].wlan[*].mib.vhtSupported = !${missing} +*.sta[0].wlan[*].agent.startingTime = 0s +*.sta[1].wlan[*].agent.startingTime = 10ms +*.sta[*].wlan[*].agent.channelsToScan = "0" +*.sta[*].wlan[*].agent.probeDelay = 1ms +*.sta[*].wlan[*].agent.minChannelTime = 5ms +*.sta[*].wlan[*].agent.maxChannelTime = 5ms +*.sta[*].wlan[*].agent.authenticationTimeout = 20ms +*.sta[*].wlan[*].agent.associationTimeout = 20ms + +%file: check.py +from pathlib import Path +text = Path('test.out').read_text() +for qos in (0, 1): + for missing in (0, 1): + assert text.count(f'VHT exchange verified: qos={qos} missing={missing}') == 1 +%postrun-command: python3 check.py diff --git a/tests/unit/Ieee80211VhtMgmtElements_1.test b/tests/unit/Ieee80211VhtMgmtElements_1.test new file mode 100644 index 00000000000..6ac5dd04684 --- /dev/null +++ b/tests/unit/Ieee80211VhtMgmtElements_1.test @@ -0,0 +1,176 @@ +%description: +Verify IEEE 802.11-2024 9.4.2.156/157 byte encodings, directional MCS maps, +operation decoding, duplicate/truncated elements and subtype restrictions. + +%includes: +#include "inet/common/packet/Packet.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211VhtMgmtElements.h" +using namespace inet; +using namespace inet::ieee80211; + +%global: +static Ptr response() +{ + auto frame = makeShared(); + frame->setAid(1); + frame->setStatusCode(SC_SUCCESSFUL); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + frame->setSupportedRates(rates); + Ieee80211VhtCapabilities cap; + cap.rxMaxMcs = {9, 8, 7, -1, -1, -1, -1, -1}; + cap.txMaxMcs = {7, -1, -1, -1, -1, -1, -1, -1}; + cap.supported160Mhz = true; + cap.shortGi80 = true; + cap.rxHighestLongGiRateMbps = 1234; + cap.txHighestLongGiRateMbps = 567; + setVhtCapabilities(frame, cap); + Ieee80211VhtOperation op; + setVhtOperation(frame, op); + frame->setChunkLength(B(30)); + return frame; +} +static std::vector bytes(const Ptr& frame) +{ + Packet packet("wire", frame); + return packet.peekAllAsBytes()->getBytes(); +} +static Ptr decode(const std::vector& data) +{ + Packet packet("wire", makeShared(data)); + return packet.popAtFront(B(data.size()), Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE); +} + +template +static void checkSubtype(int baseLength, bool operationAllowed) +{ + // IEEE Std 802.11-2024, Tables 9-62 and 9-64 through 9-69. + // Exercise the codec's subtype permissions independently of advertisement policy. + auto reference = response(); + for (bool ht : {false, true}) { + for (bool vht : {false, true}) { + auto frame = makeShared(); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + frame->setSupportedRates(rates); + if (auto response = dynamicPtrCast(frame)) { + response->setStatusCode(SC_SUCCESSFUL); + response->setAid(1); + } + if (auto beacon = dynamicPtrCast(frame)) + beacon->setBeaconInterval(SimTime(102400, SIMTIME_US)); + frame->setHtCapabilitiesPresent(ht); + frame->setVhtCapabilities(reference->getVhtCapabilities()); + frame->setVhtOperation(reference->getVhtOperation()); + frame->setVhtCapabilitiesPresent(vht); + frame->setVhtOperationPresent(vht && operationAllowed); + frame->setChunkLength(B(baseLength + (ht ? 28 : 0) + (vht ? 14 : 0) + (vht && operationAllowed ? 7 : 0))); + auto encoded = bytes(frame); + Packet packet("subtype", makeShared(encoded)); + auto decoded = packet.popAtFront(B(encoded.size())); + ASSERT(!decoded->isIncorrect()); + ASSERT(decoded->getHtCapabilitiesPresent() == ht); + ASSERT(decoded->getVhtCapabilitiesPresent() == vht); + ASSERT(decoded->getVhtOperationPresent() == (vht && operationAllowed)); + if (!operationAllowed) { + // A well-formed VHT Operation IE is forbidden in a request, + // even when the same request accepts VHT Capabilities. + encoded.insert(encoded.end(), {192, 5, 0, 0, 0, 0xfc, 0xff}); + Packet malformed("forbidden-operation", makeShared(encoded)); + ASSERT(malformed.popAtFront(B(encoded.size()), Chunk::PF_ALLOW_INCORRECT)->isIncorrect()); + } + } + } + if (!operationAllowed) { + auto forbidden = makeShared(); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + forbidden->setSupportedRates(rates); + forbidden->setVhtOperation(reference->getVhtOperation()); + forbidden->setVhtOperationPresent(true); + forbidden->setChunkLength(B(baseLength + 7)); + bool rejected = false; + try { bytes(forbidden); } catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + } +} +%activity: +checkSubtype(17, true); +checkSubtype(17, true); +checkSubtype(9, true); +checkSubtype(9, true); +checkSubtype(5, false); +checkSubtype(9, false); +checkSubtype(15, false); +auto original = response(); +auto data = bytes(original); +ASSERT(data.size() == 30); +ASSERT(data[9] == 191 && data[10] == 12); +ASSERT(data[11] == 0x24 && data[12] == 0 && data[13] == 0 && data[14] == 0); +ASSERT(data[15] == 0xc6 && data[16] == 0xff); // Rx: MCS9,8,7,unsupported +ASSERT(data[17] == 0xd2 && data[18] == 0x04); +ASSERT(data[19] == 0xfc && data[20] == 0xff); // Tx: MCS7,unsupported +ASSERT(data[21] == 0x37 && data[22] == 0x02); +ASSERT(data[23] == 192 && data[24] == 5); +ASSERT(data[25] == 0 && data[26] == 0 && data[27] == 0); +ASSERT(data[28] == 0xfc && data[29] == 0xff); +auto decoded = decode(data); +ASSERT(!decoded->isIncorrect()); +Ieee80211VhtCapabilities cap; +Ieee80211VhtOperation op; +ASSERT(decodeVhtCapabilities(decoded, cap)); +ASSERT(cap.rxMaxMcs[0] == 9 && cap.rxMaxMcs[1] == 8 && cap.rxMaxMcs[2] == 7); +ASSERT(cap.txMaxMcs[0] == 7 && cap.txMaxMcs[1] == -1); +ASSERT(cap.rxHighestLongGiRateMbps == 1234 && cap.txHighestLongGiRateMbps == 567); +ASSERT(cap.shortGi80 && !cap.shortGi20 && !cap.shortGi40 && !cap.shortGi160); +ASSERT(decodeVhtOperation(decoded, op) && op.channelWidth == MHz(20)); +auto fresh = response(); +fresh->setVhtCapabilities(decoded->getVhtCapabilities()); +fresh->setVhtOperation(decoded->getVhtOperation()); +ASSERT(bytes(fresh) == data); +for (int idOffset : {9, 23}) { + auto duplicate = data; + duplicate.insert(duplicate.end(), data.begin() + idOffset, data.begin() + idOffset + data[idOffset + 1] + 2); + ASSERT(decode(duplicate)->isIncorrect()); + auto wrongLength = data; + wrongLength[idOffset + 1]--; + ASSERT(decode(wrongLength)->isIncorrect()); +} +for (size_t length = 10; length < data.size(); length++) { + if (length == 23) + continue; // complete capability IE without optional operation + auto truncated = data; + truncated.resize(length); + ASSERT(decode(truncated)->isIncomplete()); +} +auto invalidWidth = data; +invalidWidth[11] |= 12; +ASSERT(decode(invalidWidth)->isIncorrect()); +auto frame = response(); +auto element = frame->getVhtOperation(); +element.channelWidth = 1; +element.centerFrequencySegment0 = 42; +frame->setVhtOperation(element); +ASSERT(decodeVhtOperation(frame, op) && op.channelWidth == MHz(80)); +element.centerFrequencySegment1 = 50; +frame->setVhtOperation(element); +ASSERT(decodeVhtOperation(frame, op) && op.channelWidth == MHz(160)); +element.centerFrequencySegment1 = 106; +frame->setVhtOperation(element); +ASSERT(!decodeVhtOperation(frame, op)); // 80+80 is outside this implementation + +auto forbidden = makeShared(); +forbidden->setVhtCapabilitiesPresent(true); +forbidden->setVhtCapabilities(original->getVhtCapabilities()); +forbidden->setChunkLength(B(16)); +bool rejected = false; +try { bytes(forbidden); } catch (const cRuntimeError&) { rejected = true; } +ASSERT(rejected); +std::cout << "VHT management wire contract verified.\n"; + +%contains: stdout +VHT management wire contract verified. diff --git a/tests/unit/Ieee80211VhtPeerModeSelection_1.test b/tests/unit/Ieee80211VhtPeerModeSelection_1.test new file mode 100644 index 00000000000..f2abc096a00 --- /dev/null +++ b/tests/unit/Ieee80211VhtPeerModeSelection_1.test @@ -0,0 +1,77 @@ +%description: +Verify local-Tx/peer-Rx VHT selection over every catalog mode with asymmetric +maps, all operational widths, GI restrictions, absent peer fallback and rate caps. + +%includes: +#include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +%activity: +auto modes = Ieee80211ModeSet::getModeSet("ac"); +Ieee80211VhtCapabilities local; +local.txMaxMcs = {8, 7, -1, -1, -1, -1, -1, -1}; +local.rxMaxMcs = {7, -1, -1, -1, -1, -1, -1, -1}; +local.supported160Mhz = true; +local.shortGi20 = local.shortGi40 = local.shortGi80 = local.shortGi160 = true; +Ieee80211VhtOperation operation; +Ieee80211Mib::PeerVhtState peer; +peer.advertisedCapabilities.rxMaxMcs = {7, 8, -1, -1, -1, -1, -1, -1}; +peer.advertisedCapabilities.txMaxMcs = {9, -1, -1, -1, -1, -1, -1, -1}; +peer.advertisedCapabilities.supported160Mhz = true; +int checked = 0; +for (int width : {20, 40, 80, 160}) { + operation.channelWidth = MHz(width); + peer.operation.channelWidth = MHz(width); + for (bool shortGi : {false, true}) { + auto& remote = peer.advertisedCapabilities; + remote.shortGi20 = remote.shortGi40 = remote.shortGi80 = remote.shortGi160 = shortGi; + for (int i = 0; i < modes->getNumModes(); i++) { + auto requested = modes->getMode(i); + if (requested->getVhtMcsIndex() < 0) + continue; + auto selected = selectPeerCompatibleVhtMode(modes, local, operation, &peer, requested); + ASSERT(modes->containsMode(selected)); + ASSERT(selected->getVhtMcsIndex() >= 0); + int nss = selected->getDataMode()->getNumberOfSpatialStreams(); + ASSERT(nss <= 2); + ASSERT(selected->getVhtMcsIndex() <= local.txMaxMcs[nss - 1]); + ASSERT(selected->getVhtMcsIndex() <= remote.rxMaxMcs[nss - 1]); + ASSERT(selected->getDataMode()->getBandwidth() <= MHz(width)); + ASSERT(shortGi || selected->getDataMode()->getGuardInterval() == SimTime(800, SIMTIME_NS)); + ASSERT(selected->getDataMode()->getNetBitrate() <= requested->getDataMode()->getNetBitrate()); + auto fallback = selectPeerCompatibleVhtMode(modes, local, operation, nullptr, requested); + ASSERT(fallback->getVhtMcsIndex() < 0 && fallback->getHtMcsIndex() < 0); + checked++; + } + } +} +ASSERT(checked == 4960); +operation.channelWidth = MHz(160); +peer.operation.channelWidth = MHz(20); +auto requested = modes->getFastestMode(); +auto selected = selectPeerCompatibleVhtMode(modes, local, operation, &peer, requested); +ASSERT(selected->getDataMode()->getBandwidth() == MHz(20)); +peer.advertisedCapabilities.rxHighestLongGiRateMbps = 20; +local.txHighestLongGiRateMbps = 10; +peer.advertisedCapabilities.shortGi20 = false; +selected = selectPeerCompatibleVhtMode(modes, local, operation, &peer, requested); +ASSERT(selected->getDataMode()->getNetBitrate() <= Mbps(10)); +// Table 9-315 encodes 58.5 Mb/s as 58; it must not reject that mode. +local.txHighestLongGiRateMbps = 58; +peer.advertisedCapabilities.rxHighestLongGiRateMbps = 58; +const IIeee80211Mode *fractional = nullptr; +for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + auto data = mode->getDataMode(); + if (mode->getVhtMcsIndex() == 6 && data->getBandwidth() == MHz(20) && + data->getNumberOfSpatialStreams() == 1 && data->getGuardInterval() == SimTime(800, SIMTIME_NS)) + fractional = mode; +} +ASSERT(fractional != nullptr); +ASSERT(selectPeerCompatibleVhtMode(modes, local, operation, &peer, fractional) == fractional); +std::cout << "VHT directional selection verified for 4960 requests.\n"; + +%contains: stdout +VHT directional selection verified for 4960 requests. From 6c1176abee6fb05e741b0697fafb921a78729363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 19 Sep 2026 01:05:44 +0200 Subject: [PATCH 21/21] plan: add+change: record verified HT/GI comment closure Tie the focused build, unit, module, protocol and fingerprint evidence to the pinned base and exact source/test patch. Preserve the initial fixture failures and their corrected reruns, and record the existing full-interface gate findings separately. Mark the earlier V3 report as historical. Debug/release builds, nine unit cases, 28 module cases, four protocol cases and two legacy fingerprints pass in the final source tree. Both scoped architecture checks and the mode-interface check pass. Change: plan | behavior.add+change | - --- .../done/80211htcapop-refactor-v3-evidence.md | 5 + plan/done/ht-gi-devin-comment-closure.md | 111 +++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/plan/done/80211htcapop-refactor-v3-evidence.md b/plan/done/80211htcapop-refactor-v3-evidence.md index 9e30289619b..d6d8b083689 100644 --- a/plan/done/80211htcapop-refactor-v3-evidence.md +++ b/plan/done/80211htcapop-refactor-v3-evidence.md @@ -1,5 +1,10 @@ # HT capability/BSS refactor V3: implementation evidence +This is historical evidence for the tree identified below. Fresh verification of the current +branch plus the Devin-comment fixes is recorded in +[HT/GI Devin comment closure](ht-gi-devin-comment-closure.md), including exact tree identity, +focused reruns, and the corrected discovery-test assertion. + Date: 2026-09-18. Baseline: `98117c3257b2e11661d2baf685c18911c8639b24`. Tested implementation: the uncommitted working tree following that baseline. No commits, recorded fingerprints, generated fingerprint expectations, or sealed packet-core sources were changed. diff --git a/plan/done/ht-gi-devin-comment-closure.md b/plan/done/ht-gi-devin-comment-closure.md index 2b4fdbb9f2e..c4a8ff531a0 100644 --- a/plan/done/ht-gi-devin-comment-closure.md +++ b/plan/done/ht-gi-devin-comment-closure.md @@ -1,6 +1,8 @@ # HT/GI Devin comment closure -Status: planned. Verification evidence will be recorded after implementation. +Status: completed, with the pre-existing full-interface gate findings recorded below. + +Base: `dffd66845707303f661ef148e9bca2c3cca94262`. Scope: the five comments assessed in this session. ## Implementation contract @@ -25,3 +27,110 @@ Status: planned. Verification evidence will be recorded after implementation. Extend the transition fixture with rejection followed by successful explicit retry, and VHT codec coverage for request-operation rejection and independent HT/VHT presence. +Self-validation: owners, callers, base-class implementations, fixture names and runner interfaces +checked before editing. No affected source path is sealed. One deterministic run/seed is sufficient +for these API and codec invariants. No baseline update, commit, or push is part of this task. + +## Standards + +IEEE Std 802.11-2024: 10.6.13.1/.2 (directional VHT MCS/NSS and long-GI rate limits), +10.17 (short GI), 9.4.2.156.2 and Table 9-313 (width capability), 11.38.1 (BSS width), +Table 9-315 (rate field encoding). Subtype permissions: 9.3.3.2 and 9.3.3.5–10, +Tables 9-62 and 9-64–69. Retrieved from the local standards corpus; no normative behavior changes +are intended by adding citations. The selector's rate ceiling and fallback are model policy. + +## Evidence + +Date: 2026-09-19. Tested tree: base commit above plus the source/test patch with SHA-256 +`cd580f78b349b24d84077b135a674591f2d3f01d6a6c518116c0206a277753b3`. +The patch and per-file hashes are retained in `report/ht-gi-devin-closure/source.patch` and +`source-manifest.json`. These identify an uncommitted tree, not an unchanged HEAD. +The historical V3 evidence remains historical. + +All commands ran at the checkout root except the fingerprint wrapper, run in `tests/fingerprint`. +OMNeT++ 6.4.0aipre2 / clang; debug libraries and runtime assertions for behavioral tests. +Module/protocol fixtures use their checked-in deterministic configuration, run 0 and seed 0/default; +the failure fixture deliberately exercises its 14 configuration combinations and asserts fatal outcomes. +Fingerprints use run 0, seed 0/default, and 10 seconds simulated time. + +| Gate | Outcome | Log under `/tmp/` (also archived in `report/ht-gi-devin-closure/logs/`) | +|---|---|---| +| `make -j12 MODE=debug` | PASS, exit 0; final incremental build also exit 0 | `ht-gi-closure-debug.log`, `ht-gi-closure-debug-final.log` | +| `make -j12 MODE=release` | PASS, exit 0 | `ht-gi-closure-release.log` | +| Focused units | Initial 8 PASS / 1 FAIL, exit 1; corrected VHT fixture rerun PASS, exit 0; all 9 selected cases now pass | `ht-gi-closure-unit.log`, `ht-gi-closure-vht-codec-retest.log` | +| Focused modules | Initial 27 PASS / 1 FAIL, exit 1; corrected stale discovery assertion rerun PASS, exit 0; all 28 selected cases now pass | `ht-gi-closure-module.log`, `ht-gi-closure-discovery-retest.log` | +| Focused protocols | 4 PASS, exit 0; no declared expected failures | `ht-gi-closure-protocol.log` | +| Legacy fingerprints | 2 PASS, all three ingredients match, exit 0 | `ht-gi-closure-fingerprint.log` | +| Scoped MAC architecture | PASS, exit 0 | `ht-gi-closure-architecture-mac.log` | +| Scoped PHY architecture | PASS, exit 0 | `ht-gi-closure-architecture-phy.log` | +| Mode interfaces | 5 PASS, exit 0 | `ht-gi-closure-mode-interfaces.log` | +| Full interfaces | FAIL, exit 1: 15 violations in unchanged files; not waived | `ht-gi-closure-interfaces.log` | +| `git diff --check` | PASS, exit 0 | No output | + +The first VHT fixture failed with `Invalid VHT MCS map entry 0`: generated wire-element defaults +are not legal VHT MCS map entries. The fixture now copies the valid reference elements already +used by the byte-encoding checks. The production codec required no further change. +The first discovery fixture failed at `dcfMulticastMode->getHtMcsIndex() >= 0`; see the scoped +correction below. Only those two failed cases were rerun after test-only corrections; the INET +library and other cases remained unchanged. No recorded fingerprint, statistical or stdout +expectation was rewritten. + +Exact selectors and commands: + +```bash +UNIT='Ieee80211(HtCapabilities_1|HtMgmtElements_1|HtModeSet_1|PeerModeSelection_1|MibAssociationId_1|HtGuardInterval_1|VhtModeSet_1|VhtMgmtElements_1|VhtPeerModeSelection_1)\.test$' +MODULE='Ieee80211(HtAssociation_1|HtAntennaRateControl_1|HtCapabilityPreparation_1|ConfigurationContracts_1|MgmtStaBeaconUpdate_1|MgmtApReassociationSnapshot_1|MgmtStaSimplifiedInitialization_1|MgmtAp(Lifecycle|Timeout|QueueDrop|HcfQueueDrop|HcfRtsTimeout|ChannelChange|GenericRadio|UnavailableChannel|MalformedHtCap)_1|MgmtSta(Lifecycle|Deauthentication|Disassociation|Discovery)_1|AgentStaReassociation_1|ModeSet(Transition|Failure|Registration|Retry)_1|ContentionModeSet_1|TxopModeSet_1|VhtAssociation_1)\.test$' +inet_run_unit_tests -m debug -f "$UNIT" +inet_run_module_tests -m debug -f "$MODULE" +inet_run_unit_tests -m debug -f 'Ieee80211VhtMgmtElements_1\.test$' +inet_run_module_tests -m debug -f 'Ieee80211MgmtStaDiscovery_1\.test$' +inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'Wifi(HtAssociation|Association|Reassociation|Deauth)\.test$' +# cwd: tests/fingerprint +./fingerprinttest -d -m '/examples/adhoc/qos/ .* -c Mac(NonQos|Qos) -r 0 ' -f tplx -f '~tNl' -f '~tND' examples.csv +# cwd: repository root +doc/project/enforcement/check-architecture.sh src/inet/linklayer/ieee80211 +doc/project/enforcement/check-architecture.sh src/inet/physicallayer/wireless/ieee80211 +doc/project/enforcement/check-interfaces.sh src/inet/physicallayer/wireless/ieee80211/mode +doc/project/enforcement/check-interfaces.sh +``` + +Python runners used `MPLCONFIGDIR=/tmp/ht-gi-closure-matplotlib` after initial setup to avoid an +unwritable user cache directory. The optional missing `py4j` IDE integration did not prevent tests. + +## Self-audit and limits + +- Radio preflight is read-only and occurs before either guard. The transmitter setter uses the same + resolution logic, retaining same/null-catalog behavior and virtual setter dispatch. Post-mutation + failures remain fatal; the existing failure fixture passes all its expected-error checks. +- The transition fixture checks both PHY catalogs, registered consumers, the selected transmit mode, + peer-cache identity and notification counts after repeated rejection, then checks a successful + explicit retry. It uses a 40 MHz catalog mode without claiming real 40 MHz packet-PHY support. +- VHT serializer read/write permissions are distinct and symmetric. All seven subtype cases cover + independent HT/VHT capabilities presence; requests reject VHT Operation on encoding and decoding. + Band/capability/role advertisement validation is outside this subtype-mask change. +- The interface declares pure duration operations; the existing base provides identical defaults. + HT/VHT overrides retain their timing authority. Existing legacy `_get*` wrappers retain AS-03. +- Reviewed C++ dispatch/query purity, OMNeT++ synchronous notification and reentrancy, INET ownership + and codec paths, and WLAN mode identity and standards traceability. No new exception or sealed-path + edit. No packet-core change, baseline update, commit, or push. +- This is focused verification and author self-audit, not an independent review or a clean + repository-wide compliance verdict. The full interface gate still has 15 pre-existing findings in 14 headers, all byte-identical to + HEAD (`ht-gi-closure-interface-provenance.log`). + + +### Required scope adjustment found by verification + +The 28-module run found one stale assertion in `Ieee80211MgmtStaDiscovery_1.test`: it still +requires HT group transmission, predating `ea818d0631` (legacy basic group rates). The source path +selects the fastest mandatory legacy mode for this fixture, 24 Mbps. IEEE 802.11-2024 10.6.5.4 +requires non-HT transmission from the nonempty basic legacy set. Update those two assertions and +their explanatory comment, retaining unicast checks and recorded stdout. This is a test-code +correction, not regeneration of fingerprint/statistical/output baselines. No source seal applies. +Revalidate with the same discovery module only; production code is unchanged by this adjustment. + +## Commit preparation + +On the user's subsequent commit request, the verified changes were divided by decision into +new commits. Release and migration notes were added for preflight retry behavior and the pure +duration queries; these documentation-only additions do not change the tested source/test patch. +The source/test patch identity above remains relative to the pinned base, across the entire series.