From 7431be3f5ca2850e0b5d5e69d55b682e69bd9400 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 01/91] 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 aaa369f76a8ad28e711ae6535d4a95cf1d2ad96f 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 02/91] 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 | 519 ++++++++++++++++++ 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, 2586 insertions(+), 762 deletions(-) create mode 100644 plan/done/80211htcapop-refactor-v3-evidence.md create mode 100644 plan/done/80211htcapop-refactor-v3.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 new file mode 100644 index 00000000000..a1bf2cd6491 --- /dev/null +++ b/plan/done/80211htcapop-refactor-v3.md @@ -0,0 +1,519 @@ +# Implementation plan: IEEE 802.11 HT capability, BSS, and peer-state contracts + +> **Status:** complete · **Source:** [Proposal V3](../../report/80211htcapop-refactor-v3.md) · **Prepared:** 2026-09-18 + +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 c913a63a8335ca39a319cb2089daf835ea5e95cd 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 03/91] 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 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 04/91] 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 05/91] 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 06/91] 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 07/91] 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 08/91] 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 09/91] 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 10/91] 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 11/91] 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 12/91] 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 13/91] 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 14/91] 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 15/91] 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 16/91] 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 17/91] 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 18/91] 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 19/91] 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 20/91] 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 21/91] 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 22/91] 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 23/91] 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 24/91] 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. From 9ff0fb1be8b9b1a0ef848df7b44a9efc33bc374e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:33:59 +0200 Subject: [PATCH 25/91] tests.fingerprint: change: align inherited graphical expectations Retain the previously accepted upstream graphical re-recording and refresh its three Block Ack-related tokens on the rebased master 7287f347aa. MacQosWithBlockAck, HCFfragblockack and txop/General produce the same values on master, this baseline-only commit and the pre-ADDBA checkpoint. Their nonvisual fingerprints already match master and remain unchanged here. Upstream 9a9e65cbb5 corrected BAR length and wire encoding after the earlier recording. These three graphical values are inherited drift, so they belong in this prerequisite rather than a later protocol change. The other seven previously accepted graphical updates are retained. The user approved this exact correction after the 2026-09-15 attribution report. Scoped debug verification uses run 0 and all declared ingredients. Change: tests.fingerprint | behavior.change | - | pr-1148-resolve-audit-findings --- tests/fingerprint/examples.csv | 10 +++++----- tests/fingerprint/showcases.csv | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 64abe9fcac0..7e75b7105f6 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -6,8 +6,8 @@ /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping1 -r 0, 1000s, 8668-d4e7/tplx;e2b1-205b/~tNl;4083-075c/~tND;e1f1-f93c/tyf, PASS, wireless adhoc Ipv4 # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 1486-cbae/tplx;521b-977a/~tNl;f7ee-50fd/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, cca6-4388/tplx;d036-f044/~tNl;a0e3-f6ff/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 1486-cbae/tplx;521b-977a/~tNl;f7ee-50fd/~tND;e391-4c6d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, cca6-4388/tplx;d036-f044/~tNl;a0e3-f6ff/~tND;507d-21cb/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 9c66-e6f0/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 85bd-85c6/tplx;22f6-085d/~tNl;e4ea-962c/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -655,10 +655,10 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;7a95-cf35/~tND, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, c3c0-dc93/tplx;5eb9-6cea/~tNl;87a5-11a1/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 3a58-19c0/tplx;6188-6ffd/~tNl;377c-d4fc/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 3a58-19c0/tplx;6188-6ffd/~tNl;377c-d4fc/~tND;100e-198d/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 33e5-c380/tplx;78d2-bb9a/~tNl;1524-d88f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 7f3b-72ca/tplx;7c57-849b/~tNl;06e3-7901/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 6f59-bd7d/tplx;c3ce-3389/~tNl;9e1e-7c17/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 7f3b-72ca/tplx;7c57-849b/~tNl;06e3-7901/~tND;2fc2-2ac9/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 6f59-bd7d/tplx;c3ce-3389/~tNl;9e1e-7c17/~tND;6880-4ff1/tyf, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 330453edd7b..e72c62f8d96 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -266,8 +266,8 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 57ee-7ddf/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, f7a5-cf0a/tplx;a24a-a070/~tNl;5dae-0b8e/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 5abb-93a6/tplx;2bdd-d1fa/~tNl;31ab-28b7/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, f7a5-cf0a/tplx;a24a-a070/~tNl;5dae-0b8e/~tND;11a9-72e7/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 5abb-93a6/tplx;2bdd-d1fa/~tNl;31ab-28b7/~tND;a065-ca9a/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 3623-0ff6/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -313,8 +313,8 @@ /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/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, 26be-ff05/tplx;9c39-00a3/~tNl;03a9-2f02/~tND;5fef-ddb3/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 8d7c-5091/tplx;84a6-b893/~tNl;cb18-5b4d/~tND;6ba7-4c7c/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 @@ -341,5 +341,5 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb5-d354/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, eb27-b910/tplx;7c61-294a/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, eb27-b910/tplx;7c61-294a/~tNl;29d5-c1ad/tyf, PASS, wireless Ipv4 From 5637f1955b84a6eb3411fff66f7621c1d7b2af37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:34:47 +0200 Subject: [PATCH 26/91] plan: add: resolve the PR1148 audit findings Record the authorized repair, evidence and reconstruction requirements before referring to this plan from implementation commits. Change: plan | behavior.add | - | pr-1148-resolve-audit-findings --- .../pending/pr-1148-resolve-audit-findings.md | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 plan/pending/pr-1148-resolve-audit-findings.md diff --git a/plan/pending/pr-1148-resolve-audit-findings.md b/plan/pending/pr-1148-resolve-audit-findings.md new file mode 100644 index 00000000000..39f816f6b0f --- /dev/null +++ b/plan/pending/pr-1148-resolve-audit-findings.md @@ -0,0 +1,262 @@ +# Resolve the audit findings of PR #1148 + +Status: **approved reconstruction in progress** — F1/F2 and interface repairs, isolated +rebase, and all 17 diagnostic source-prefix checks are complete. Final history +authoring and verification are in progress after explicit acceptance of the exact baseline and history proposals. +Prepared: 2026-09-14. +PR: [INET #1148](https://github.com/inet-framework/inet/pull/1148). +Audit evidence: local, ignored `audit/pull-request/pr-1148.md`, +`pr-1148-summary.md`, and `pr-1148-evidence/` beside them. + +Reviewed head: `991f626a4f5a031d5b507f6ff430cca36fb794ce`. +Reviewed merge base: `cbbba4d487649cfbef8bc4d7c225452699eb563c`. +Branch: `cleanup/fix-ieee80211-addba-transaction-minimal`. + +## 1. Outcome and order + +Make the ADDBA transaction series safe under synchronous queue callbacks, complete its logical +queue-departure contract, and produce a tested, reviewable series against a pinned upstream base. +The audit reproduced two simulator crashes and one missing departure notification; the existing +eight focused tests passed and therefore do not cover these failures. + +Execute in this order: preserve the reviewed checkpoint → repair F1 → repair F2 → repair interface +contracts → validate the repaired checkpoint → rebase in isolation → attribute baselines and +reconstruct the final series → verify and refresh the review artifacts. + +| Finding | Required change | Completion evidence | +| --- | --- | --- | +| F1, P1 | Make both agreement-expiry loops safe when callbacks erase or replace agreements | Both crash regressions pass; real HCF queue-overflow regression passes | +| F2, P2 | Publish exactly one logical departure for packets rejected before a compound queue's leaf queue | Deterministic RED regression and HCF transaction cleanup regression pass | +| S1 | Attribute each fingerprint transition and place it with its causal source change | Per-row evidence and passing intermediate commits | +| S2 | Split C12's null BAR result guard from its recipient timeout-policy correction | Independently testable commits | +| S3 | Supply C17's missing classification trailer | Valid `Change:` trailer; classify all newly authored commits | +| S4 | Remove newly introduced interface method bodies | Explicit implementations; interface scans and feature builds | +| S5 | Give queue-removal reasons explicit stable values | Existing numeric values preserved; departure tests pass | +| S6 | Restore unrelated whitespace hunks | Per-commit whitespace inspection | + +Subject scope/kind prefixes are optional under +[CR-TAG-SUBJECT](../../doc/project/rule/classification.md#cr-tag-subject). +S3 does not require adding a subject prefix. C01–C16 predate that classification rule; their +original missing trailers are not retroactive findings. + +## 2. Preserve evidence and establish the starting tree + +- [x] Record HEAD, base, working-tree state, toolchain, build mode and feature configuration. + Preserve a local ref to the reviewed head before any subsequent history operation. +- [x] Keep the existing audit and failing-probe artifacts intact. Store new results in a separate + directory keyed by the candidate commit and phase. +- [x] Check the current seal registry for the actual repair paths. The audited diff touches no + sealed source path; expanding the repair surface requires checking again. +- [x] Turn the diagnostic cases into small maintained regressions. Avoid copying the probes' + large ADDBA harness wholesale. Each regression must demonstrate its intended failure on the + reviewed head before applying its repair. + +## 3. Repair F1: agreement expiry under synchronous callbacks + +Primary files: `src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc` +and `RecipientBlockAckAgreementHandler.cc` in the same directory; focused unit/module tests. + +- [x] Snapshot expired agreement identities as values: peer, TID and generation. Do not retain + map iterators or agreement pointers as the work list. +- [x] Before processing each identity, look it up again and confirm generation, state and expiry. + Skip entries deleted, replaced or made ineligible by an earlier callback. +- [x] Inspect every outward call in the expiry path, including originator + `releaseBlockAckAgreementFrames` and both roles' `processMgmtFrame`. Capture necessary values + before the call; relookup before any subsequent access to live agreement state. +- [x] Preserve the generation attached to DELBA. A callback that installs a replacement agreement + at the same peer/TID must not cause teardown of that replacement. +- [x] Preserve shared inactivity-timer scheduling after callbacks alter the agreement collection. + Advancing the iterator before calling out is insufficient: a callback may erase the next entry. + +Tests and acceptance: + +- [x] Both roles: the existing expiry probes' erase-current callback completes without a crash. +- [x] Two expired agreements: erase a sibling during the first callback; process each surviving + eligible generation at most once. Also replace the same peer/TID with a new generation. +- [x] Mix expired, future and disabled deadlines; verify teardown selection and the next shared + timer deadline, including removal of the final agreement. +- [x] Use real HCF and a bounded queue to drop the generated DELBA synchronously. Assert terminal + cleanup, absence of stale agreement use, and continued simulation progress for both roles. + +## 4. Repair F2: compound queue rejection notifications + +Primary files: `src/inet/queueing/queue/CompoundPacketQueueBase.{h,cc}`, the relevant queueing +contracts, queueing tests, and IEEE 802.11 module tests. `RedDropperQueue` is declared in +`src/inet/queueing/queue/RedMarkerQueue.ned` and provides the deterministic reproduction. + +- [x] Write a source/ownership matrix for leaf queue departures, prequeue filter drops, + compound-owned overflow, nested compounds and shared-buffer eviction. Identify which source + owns each packet and which component emits the one logical departure. +- [x] Extend the compound notification path to include drops from nonqueue components outside + the nearest child-queue boundary. Resolve emitters through generic contracts/composition; keep + IEEE 802.11 tags and concrete RED types out of generic queueing logic. +- [x] Forward a borrowed packet only while it is alive and before deletion. Do not translate every + bubbling `packetDropped` indiscriminately: leaf and shared-buffer cases can already emit a + departure. Preserve listener subscription/cleanup and existing statistics semantics. +- [x] Verify HCF/DCF consume the corrected event exactly once. Change consumer code only if the + production regression exposes a separate defect. + +Tests and acceptance: + +- [x] RED: capacity 1, `minth=0`, `maxth=1`, `wq=1`, `maxp=0`; enqueue twice without dequeue. + Assert one drop and one logical departure with the same packet identity and `DROPPED` reason. +- [x] Leaf drops, nested queues, shared-buffer eviction, selective extraction and ordinary dequeue + retain exactly-once departure semantics at each exposed logical queue boundary. +- [x] Real HCF with RED in the management access-category queue: reject a pending tagged ADDBA + request. Assert transaction removal, no response timer armed for the unsent request, no stale + eligibility entry, and resumed same-TID data eligibility under the configured policy. +- [x] Exercise terminal AP management outcomes through DCF and HCF; assert one outcome per + transaction and no duplicate cancellation/drop handling. + +## 5. Repair interface and numeric contracts + +- [x] S4: make the new methods pure in `IBlockAckAgreementHandlerCallback`, + `IFrameSequenceHandler`, `IOriginatorBlockAckAgreementHandler`, + `IRecipientBlockAckAgreementHandler`, and nested `IPacketBuffer::ICallback`. +- [x] Inventory every in-tree implementor and call site. Put required behavior in concrete + implementations, including test doubles; introduce a shared implementation base only where + actual reuse justifies it. Document any external implementor migration obligation. +- [x] Keep generic queueing contract changes separate from IEEE 802.11 contracts. Leave the + pre-existing ledgered `ITransmitStep`/`IReceiveStep` defaults outside this repair. +- [x] S5: assign `DEQUEUED=0`, `REMOVED=1`, `DROPPED=2` explicitly in + `src/inet/queueing/contract/IPacketQueue.h`; preserve those values in future changes. +- [ ] Compile affected implementations with the relevant optional features both enabled and + disabled. Run the interface scan and manually inspect nested callbacks, which the audit's + scanner did not detect. + +## 6. Proposed change units and final placement + +These are repair units, not a prescription to append fixups to the published series. During final +reconstruction, incorporate each correction and its regression into the commit introducing the +contract. Follow [PR-SERIES-ORDER](../../doc/project/rule/pull-request.md#pr-series-order). + +| Proposed subject | Single decision and rationale | Dependency / surface | Evidence and expected effect | +| --- | --- | --- | --- | +| `queueing: report prequeue rejection as a logical departure` | Complete the generic departure contract for rejected input | Existing departure API; compound queue and queueing tests; fold into C01 | RED plus composition tests; one formerly missing event, possible fingerprint movement | +| `queueing: require explicit buffer callback implementations` | Remove implicit behavior from the new callback contract | Buffer API introduction; contract and all implementors; fold into introducing unit | Interface inventory, feature builds, queueing tests; preserve behavior | +| `queueing: assign stable packet removal reason values` | Stabilize externally observed enum values | Departure API; `IPacketQueue.h`; fold into C01 | Numeric inspection and departure tests; no behavior/baseline movement | +| `ieee80211: clear transactions after compound queue rejection` | Establish cleanup through the complete producer contract | Generic departure repair; C02 consumer and later ADDBA/module coverage placed where prerequisites exist | Real RED/HCF and AP terminal-outcome tests; production edits only if needed | +| `ieee80211: make agreement expiry safe under queue callbacks` | Preserve generation identity across reentrant expiry callbacks | Agreement-generation/teardown introduction; both handlers and their tests | Two crash regressions, replacement tests and HCF overflow; possible trajectory movement | +| `ieee80211: require explicit transaction callback implementations` | Make new MAC contract obligations explicit | Respective interface introductions and every implementor | Interface scan, feature builds and transaction tests; preserve behavior | +| `ieee80211: reject null BAR defragmentation results` | Stop dispatch after unsuccessful BAR reassembly | Split source/test subset of C12 | Direct null-result regression; determine fingerprint effect from affected receive paths | +| `ieee80211: honor recipient Block Ack timeout policy` | Correct timeout policy selection | Independent source/test subset of C12 | Test both policy branches; determine timer/fingerprint effect | + +Retain unaffected original decisions in their dependency order. Do not commit failing tests before +their fixes. Add this plan to history before using +`Plan: plan/pending/pr-1148-resolve-audit-findings.md` in implementing commits. Write valid +`Change:` trailers from each actual diff, and record reproduction, rationale and evidence in the +messages. The final commit count follows the resulting decisions, not a target of 17. + +## 7. Validate, rebase and attribute expectations + +- [x] Build a fresh debug library and run the focused tests on the repaired original-base tree. + Preserve that tested checkpoint before rebasing. +- [ ] Pin the then-current upstream target. The audit observed a merge conflict against + `0c85e5dd6bc2969987286210962983b19384670e`; do not assume that is still the target. + Resolve the rebase in an isolated candidate, using the INET rebase workflow and scoped + `opp_repl` evidence. Record conflict decisions and rerun affected tests after resolution. +- [ ] Inventory the 16 fingerprint rows added/changed by the audited series, then recompute the + inventory against the pinned new base. Map changed production paths through dependency data + to affected configurations, runs and seeds; include all declared ingredients (`tplx`, `~tNl`, + `~tND` where applicable). +- [ ] For each moving row, record configuration/run, old and candidate values, ingredient, + causal source commit, observed behavioral difference, correctness argument and artifact paths. + Multiple transitions of the same row need evidence at each responsible commit. +- [ ] Investigate mismatches before replacing expectations. Present the exact proposed baseline + changes for acceptance under [change-a-baseline.md](../../doc/project/guide/change-a-baseline.md). + Candidate calculations remain evidence until the required approval is recorded. +- [ ] Reconstruct the final series on the fixed new base using the INET branch-cleanup workflow. + Split C12, fold each accepted baseline transition into its causal commit, remove standalone + C14, restore S6's incidental whitespace and complete S3's trailers. Do not squash all baseline + changes into C13 merely because it precedes C14. +- [ ] Verify each final intermediate tree with its directly applicable build/tests and scoped + fingerprints. Compare the reconstructed final source tree with the tested post-rebase tree; + account explicitly for intended whitespace, documentation and baseline differences. + +## 8. Final verification and handoff + +Use the active test/build skill interfaces when executing. The existing focused filters are: + +```bash +inet_run_unit_tests -m debug -f '(Ieee80211AddbaTransaction_1|Ieee80211MgmtFrameSerializer_1|Ieee80211MgmtTransactionTag_1)\.test$' +inet_run_module_tests -m debug -f '(Ieee80211BlockAckInactivityTimer_1|Ieee80211MgmtApCancellation_1|Ieee80211MgmtApHcfQueueDrop_1|Ieee80211MgmtApQueueDrop_1)\.test$' +inet_run_queueing_tests -m debug -f 'PacketQueueDepartureSignal_1\.test$' +``` + +- [ ] Add explicit filters for the new regressions and retain commands, mode, seeds, exit status + and logs. Repeat tests after relevant changes; do not reuse pre-rebase results as final evidence. +- [ ] Complete debug and release compilation and the affected feature matrix using the supported + build workflow. Run source-seal, architecture, interface, naming, classification, commit and + whitespace gates as described in [run-the-gates.md](../../doc/project/guide/run-the-gates.md). +- [ ] Resolve the audit's usage/override inventory and affected standards-traceability gaps. + Inspect authoritative local clauses for claims this series makes; record unavailable evidence + explicitly rather than marking it passed. Keep pre-existing ledgered findings separate. +- [ ] Refresh the audit and commit summary against the final exact head/base. Close F1/F2 only + with the production-path tests above; close S1 only with per-row and per-commit evidence. +- [ ] Correct the PR description: malformed AID handling is + `Ieee80211MgmtFrameSerializer::decodeAssociationId` calling `markIncorrect`; the teardown tag + carries `generationId`, while role/peer/TID come from DELBA/context. Include actual reading + order, architectural/API surface, baseline causes and verified test scope. +- [ ] Hand off the final diff, revised description and evidence table for review. Move the plan + to `plan/done/` only after its required repairs and verification are complete. + +Completion means both reproduced defects are covered and repaired, all six policy findings are +resolved, the series builds and passes its applicable tests at every commit, and the final audit +clearly distinguishes verified claims from any remaining evidence limitations. + +## Execution checkpoint — 2026-09-15 + +Reviewed head preserved at `archive/pr-1148-reviewed-991f626a4f`. The original-base +repair checkpoint is `d3045c9780`, preserved at +`archive/pr-1148-repaired-original-base`. Repair commits: `6adbd1d53f` (F2/S5), +`75a0e20b24` (F1), `fec8ad0ba6` (generic callback purity), `d3045c9780` (MAC purity). + +New maintained regressions: + +- `Ieee80211OriginatorExpiryReentrant_1` and `Ieee80211RecipientExpiryReentrant_1`: + crash on reviewed implementations; pass with repairs. +- `Ieee80211HcfAgreementQueueDrop_1`: real HCF expiry plus bounded management + queues crashes on reviewed implementations; repaired version passes both roles. +- `Ieee80211HcfAddbaRedDrop_1`: reviewed compound queue leaves the pending ADDBA; + repair removes it without arming a response timer and restores data eligibility. +- `CompoundPacketQueueRedDeparture_1` and + `CompoundPacketQueueCompositionDeparture_1`: deterministic rejection identity, + reason and nested/leaf/shared-buffer/dequeue/removal coverage pass. + +The existing five unit tests (including the two new expiry tests), six module +tests (including the two new HCF tests) and three queueing tests pass on the +repaired original-base tree. Debug build passes; release/feature builds ongoing. +Scoped architecture gates pass. Interface scan retains only `AV-CONTRACT-02`. + +Upstream master was verified via GitHub and pinned to +`0c85e5dd6bc2969987286210962983b19384670e`. Isolated replay at +`/tmp/inet-pr1148-rebase`, branch `repair/pr-1148-rebase-candidate`, reaches +`3eb505006b9966648c1d46b7cccd8d9d965669f5`. Only WHATSNEW conflicted; all repaired +production and focused-test paths are identical to the original-base checkpoint. +Candidate build/testing and release-note numbering normalization are pending. + +Evidence: local ignored `audit/pull-request/pr-1148-repair/991f626a4f/` and +`ai-logs/executions/2026-09-14_pr1148-rebase.md`. The original audit/probes remain +intact. No new fingerprint expectation has been accepted or written. S1, final +C12 split, S3 classification, S6 restoration, per-commit verification, and final +review handoff remain open. Do not move this plan to done at this checkpoint. + +## Diagnostic checkpoint — 2026-09-15 01:25 + +All 17 source prefixes build in debug and release and pass their available +focused cases (202 case executions). All 994 declared fingerprint ingredients +were measured, with each moving candidate reproduced. Final scope: 14 focused +cases and 59 ingredients. All final values equal the repaired rebase oracle. +C03 response-token echo was moved alongside originator validation; C12 is split; +C14 baseline-only changes are distributed to their causes. The exact baseline +and 20-commit history proposals are in the local ignored repair evidence. +No tracked expectations have been updated. Explicit acceptance, final history +authoring, per-commit verification and final handoff remain pending. + +## Accepted reconstruction — 2026-09-15 + +The user accepted the exact baseline and ordered 20-commit proposals with “yes”. +Apply inherited upstream graphical drift in a prerequisite baseline commit, +then this plan, then the 17 measured source decisions with their causal +expectations and migration notes, and finally the verified closure. Preserve +the original topic and all diagnostic checkpoints. From b914b870f42df2f772b8b279d1c2e14dff01a8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:35:09 +0200 Subject: [PATCH 27/91] queueing: add+change: expose extraction and logical departures Consumers need provider-selected predicate extraction and exactly one terminal departure at each logical queue boundary. Include prequeue filter rejection, nested composition and shared-buffer eviction without coupling generic queues to MAC transaction tags. Validation: diagnostic debug and release builds; 7 focused unit/module/queueing cases; 56 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.queueing | behavior.add+change | test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 31 +++++ doc/src/migration-guide/index.rst | 26 ++++ doc/src/users-guide/ch-diffserv.rst | 8 +- src/inet/queueing/base/PacketFlowBase.cc | 19 +++ src/inet/queueing/base/PacketFlowBase.h | 6 +- src/inet/queueing/base/PacketGateBase.cc | 14 ++ src/inet/queueing/base/PacketGateBase.h | 2 + src/inet/queueing/base/PacketQueueBase.cc | 19 +++ src/inet/queueing/base/PacketQueueBase.h | 2 + src/inet/queueing/base/PacketQueueBase.ned | 1 + src/inet/queueing/buffer/PacketBuffer.cc | 18 ++- src/inet/queueing/contract/IPacketBuffer.h | 12 ++ src/inet/queueing/contract/IPacketExtractor.h | 38 ++++++ src/inet/queueing/contract/IPacketQueue.cc | 11 ++ src/inet/queueing/contract/IPacketQueue.h | 25 +++- src/inet/queueing/contract/IPacketQueue.ned | 1 + .../contract/PacketQueueRemovalDetails.h | 24 ++++ .../queueing/queue/CompoundPacketQueueBase.cc | 128 +++++++++++++++++- .../queueing/queue/CompoundPacketQueueBase.h | 14 ++ src/inet/queueing/queue/PacketQueue.cc | 66 +++++++-- src/inet/queueing/queue/PacketQueue.h | 6 + src/inet/queueing/scheduler/LabelScheduler.cc | 102 +++++++++++++- src/inet/queueing/scheduler/LabelScheduler.h | 7 +- .../queueing/scheduler/PriorityScheduler.cc | 86 ++++++++++-- .../queueing/scheduler/PriorityScheduler.h | 6 +- src/inet/queueing/scheduler/WrrScheduler.cc | 119 +++++++++++++++- src/inet/queueing/scheduler/WrrScheduler.h | 12 +- ...oundPacketQueueCompositionDeparture_1.test | 122 +++++++++++++++++ .../CompoundPacketQueueRedDeparture_1.test | 54 ++++++++ .../PacketQueueDepartureSignal_1.test | 98 ++++++++++++++ 30 files changed, 1025 insertions(+), 52 deletions(-) create mode 100644 src/inet/queueing/contract/IPacketExtractor.h create mode 100644 src/inet/queueing/contract/IPacketQueue.cc create mode 100644 src/inet/queueing/contract/PacketQueueRemovalDetails.h create mode 100644 tests/queueing/CompoundPacketQueueCompositionDeparture_1.test create mode 100644 tests/queueing/CompoundPacketQueueRedDeparture_1.test create mode 100644 tests/queueing/PacketQueueDepartureSignal_1.test diff --git a/WHATSNEW b/WHATSNEW index 822692b152c..3d4a90f80e1 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -297,6 +297,37 @@ 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. Packet queue extraction and lifecycle signals + + IPacketQueue now supports queue-lifecycle signals and predicate-based extraction. + Direct C++ implementations of IPacketQueue must implement findPacket(), the + predicate overload of dequeuePacket(), and must emit packetQueueDeparture + with PacketQueueRemovalDetails exactly once whenever a packet is dequeued, + explicitly removed, or destructively dropped. Listeners subscribe to the queue + module and filter the source to exclude descendant queue emissions. Providers + connected to CompoundPacketQueueBase must also implement IPacketExtractor so + that a compound queue preserves the provider's scheduling policy when it + extracts a matching packet. Direct C++ + implementations of IPacketExtractor must replace selected-pointer extraction + with findPacket(predicate) and dequeuePacket(predicate); predicates may be + evaluated repeatedly and must be stable and side-effect free throughout one + logical selection. WrrScheduler, + LabelScheduler, and PriorityScheduler accept ordinary IPassivePacketSource + inputs; they require IPacketCollection only when collection/aggregate access is + used and IPacketExtractor only when predicate extraction is used, reporting the + unsupported operation lazily. + PriorityScheduler aggregate queries no longer return -1 when an input lacks + IPacketCollection; getNumPackets() and getTotalLength() now report that + unsupported operation with cRuntimeError. Update callers that treated -1 as + an unknown aggregate size, or connect collection-capable providers. + IPacketBuffer::ICallback requires explicit handlePacketDropping() and + handlePacketDropped() implementations. The first detaches the packet; the + second publishes its departure after all victims of the overload operation + have been detached. Custom buffer owners must implement both phases. Compound + queues propagate destructive drops through arbitrary non-queue wrappers and + nested compound queues exactly once. PacketBuffer rejects packets owned by a + cPacketQueue whose owner cannot participate in the buffer callback contract. + 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..0092d3b45fc 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -49,6 +49,32 @@ 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. +Packet Queue Extraction and Departure Callbacks +----------------------------------------------- + +Direct implementations of ``IPacketQueue`` must implement ``findPacket(predicate)`` and +``dequeuePacket(predicate)``. Compound-queue providers must implement +``IPacketExtractor`` for predicate selection. Select through the provider's scheduling +policy; do not use collection index order as a replacement. Predicates may be evaluated +repeatedly and must remain stable and free of side effects during selection. + +Emit ``packetQueueDeparture`` once at each logical queue boundary while the borrowed +packet is alive. Supply ``PacketQueueRemovalDetails`` with ``DEQUEUED=0``, ``REMOVED=1`` +or ``DROPPED=2``. A prequeue filter rejection is a drop even when the leaf queue never +owned the packet. Listeners on a compound queue must filter child-queue emissions to +avoid handling an outcome twice. + +Buffer owners implementing ``IPacketBuffer::ICallback`` must explicitly implement both +``handlePacketDropping()`` and ``handlePacketDropped()``. Detach a victim during the +first phase; publish its departure during the second phase, after every victim in the +overflow batch has been detached. Do not read a borrowed packet after the publisher +deletes it. + +``PriorityScheduler`` aggregate queries now throw when an input lacks +``IPacketCollection``. Replace callers that interpret ``-1`` as an unknown aggregate +size, or provide collection-capable inputs. Predicate extraction similarly requires an +extraction-capable provider only when that operation is used. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/doc/src/users-guide/ch-diffserv.rst b/doc/src/users-guide/ch-diffserv.rst index 03302b6e30c..d865b53a7bf 100644 --- a/doc/src/users-guide/ch-diffserv.rst +++ b/doc/src/users-guide/ch-diffserv.rst @@ -282,9 +282,11 @@ interface is ready to transmit one. They have several input gates and one output gate. Modules that are connected to the inputs of a scheduler must implement -the :cpp:`IPacketQueue` C++ interface. Schedulers also implement the -:cpp:`IPacketQueue` interface, so they can be cascaded to other -schedulers and used as the output module of :ned:`IPacketQueue`'s. +the :cpp:`IPassivePacketSource` C++ interface. Collection and predicate +extraction operations additionally require the corresponding input provider +to implement :cpp:`IPacketCollection` and :cpp:`IPacketExtractor`, respectively. +Schedulers can be cascaded and used as the output module of compound packet +queues when those additional interfaces are available. There are several possible scheduling disciplines (first come/first served, priority, weighted fair, weighted round-robin, deadline-based, diff --git a/src/inet/queueing/base/PacketFlowBase.cc b/src/inet/queueing/base/PacketFlowBase.cc index 3ccc24cb908..7101aa46c0e 100644 --- a/src/inet/queueing/base/PacketFlowBase.cc +++ b/src/inet/queueing/base/PacketFlowBase.cc @@ -24,6 +24,7 @@ void PacketFlowBase::initialize(int stage) provider.reference(inputGate, false); collector.reference(outputGate, false); collection.reference(inputGate, false); + packetExtractor.reference(inputGate, false); WATCH(inProgressStreamId); } else if (stage == INITSTAGE_QUEUEING) { @@ -168,6 +169,24 @@ Packet *PacketFlowBase::pullPacket(const cGate *gate) return packet; } +Packet *PacketFlowBase::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + checkPacketStreaming(nullptr); + auto packet = packetExtractor->dequeuePacket(predicate); + if (packet == nullptr) + return nullptr; + take(packet); + emit(packetPulledInSignal, packet); + processPacket(packet); + handlePacketProcessed(packet); + emit(packetPulledOutSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + Packet *PacketFlowBase::pullPacketStart(const cGate *gate, bps datarate) { Enter_Method("pullPacketStart"); diff --git a/src/inet/queueing/base/PacketFlowBase.h b/src/inet/queueing/base/PacketFlowBase.h index d0725d5e173..40f75c0bdc9 100644 --- a/src/inet/queueing/base/PacketFlowBase.h +++ b/src/inet/queueing/base/PacketFlowBase.h @@ -16,17 +16,19 @@ #include "inet/queueing/common/PassivePacketSourceRef.h" #include "inet/queueing/contract/IPacketCollection.h" #include "inet/queueing/contract/IPacketFlow.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPacketFlow, public virtual IPacketCollection +class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPacketFlow, public virtual IPacketCollection, public virtual IPacketExtractor { protected: cGate *inputGate = nullptr; ActivePacketSourceRef producer; PassivePacketSourceRef provider; ModuleRef collection; + ModuleRef packetExtractor; cGate *outputGate = nullptr; PassivePacketSinkRef consumer; @@ -82,6 +84,8 @@ class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPack virtual Packet *getPacket(int index) const override { return collection->getPacket(index); } virtual bool isEmpty() const override { return collection->isEmpty(); } virtual void removePacket(Packet *packet) override { collection->removePacket(packet); } + virtual Packet *findPacket(const PacketPredicate& predicate) const override { return packetExtractor->findPacket(predicate); } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override { collection->removeAllPackets(); } }; diff --git a/src/inet/queueing/base/PacketGateBase.cc b/src/inet/queueing/base/PacketGateBase.cc index 69d237af107..aedd6d97b03 100644 --- a/src/inet/queueing/base/PacketGateBase.cc +++ b/src/inet/queueing/base/PacketGateBase.cc @@ -98,6 +98,20 @@ void PacketGateBase::removePacket(Packet *packet) PacketFlowBase::removePacket(packet); } +Packet *PacketGateBase::findPacket(const PacketPredicate& predicate) const +{ + if (!isOpen()) + return nullptr; + auto packet = PacketFlowBase::findPacket(predicate); + return packet != nullptr && canPacketFlowThrough(packet) ? packet : nullptr; +} + +Packet *PacketGateBase::dequeuePacket(const PacketPredicate& predicate) +{ + auto packet = findPacket(predicate); + return packet == nullptr ? nullptr : PacketFlowBase::dequeuePacket([packet](const Packet *candidate) { return candidate == packet; }); +} + void PacketGateBase::removeAllPackets() { if (isOpen()) diff --git a/src/inet/queueing/base/PacketGateBase.h b/src/inet/queueing/base/PacketGateBase.h index 9ccb2701836..fc4c42175d6 100644 --- a/src/inet/queueing/base/PacketGateBase.h +++ b/src/inet/queueing/base/PacketGateBase.h @@ -53,6 +53,8 @@ class INET_API PacketGateBase : public PacketFlowBase, public TransparentProtoco virtual Packet* getPacket(int index) const override; virtual bool isEmpty() const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual IPassivePacketSink *getConsumer(const cGate *gate) override { return this; } diff --git a/src/inet/queueing/base/PacketQueueBase.cc b/src/inet/queueing/base/PacketQueueBase.cc index 4780fa44fb7..5163fc77150 100644 --- a/src/inet/queueing/base/PacketQueueBase.cc +++ b/src/inet/queueing/base/PacketQueueBase.cc @@ -7,8 +7,12 @@ #include "inet/queueing/base/PacketQueueBase.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/Simsignals.h" +#include "inet/common/PacketEventTag.h" #include "inet/common/StringFormat.h" +#include "inet/common/TimeTag.h" namespace inet { namespace queueing { @@ -53,6 +57,21 @@ Packet *PacketQueueBase::dequeuePacket() return packet; } +void PacketQueueBase::notifyPacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason) +{ + PacketQueueRemovalDetails details(reason); + emit(IPacketQueue::packetQueueDepartureSignal, packet, &details); +} + +void PacketQueueBase::recordPacketDequeued(Packet *packet) +{ + auto queueingTime = simTime() - packet->getArrivalTime(); + auto packetEvent = new PacketEvent(); + insertPacketEvent(this, packet, PEK_QUEUED, 0, queueingTime, packetEvent); + increaseTimeTag(packet, queueingTime, queueingTime); + emit(packetPulledSignal, packet); +} + void PacketQueueBase::emit(simsignal_t signal, cObject *object, cObject *details) { if (signal == packetPushedSignal || signal == packetPushStartedSignal) diff --git a/src/inet/queueing/base/PacketQueueBase.h b/src/inet/queueing/base/PacketQueueBase.h index 97fcaffd133..779c469f014 100644 --- a/src/inet/queueing/base/PacketQueueBase.h +++ b/src/inet/queueing/base/PacketQueueBase.h @@ -31,6 +31,8 @@ class INET_API PacketQueueBase : public PacketProcessorBase, public virtual IPac virtual void handleMessage(cMessage *message) override; virtual void emit(simsignal_t signal, cObject *object, cObject *details = nullptr) override; + virtual void recordPacketDequeued(Packet *packet); + virtual void notifyPacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason); virtual std::string resolveDirective(char directive) const override; diff --git a/src/inet/queueing/base/PacketQueueBase.ned b/src/inet/queueing/base/PacketQueueBase.ned index 9075cc9745a..a583ec1ce67 100644 --- a/src/inet/queueing/base/PacketQueueBase.ned +++ b/src/inet/queueing/base/PacketQueueBase.ned @@ -25,6 +25,7 @@ package inet.queueing.base; simple PacketQueueBase extends PacketProcessorBase { parameters: + @signal[packetQueueDeparture](type=inet::Packet); // Details: PacketQueueRemovalDetails; source identifies the logical queue displayStringTextFormat = default("contains {numPackets} pk ({totalLength})\npushed {numPushedPackets} pulled {numPulledPackets} dropped {numDroppedPackets}"); // Determines display string text above the submodule @display("i=block/queue"); gates: diff --git a/src/inet/queueing/buffer/PacketBuffer.cc b/src/inet/queueing/buffer/PacketBuffer.cc index a67da4be8ca..ee9a3ff847b 100644 --- a/src/inet/queueing/buffer/PacketBuffer.cc +++ b/src/inet/queueing/buffer/PacketBuffer.cc @@ -53,21 +53,29 @@ b PacketBuffer::getTotalLength() const void PacketBuffer::addPacket(Packet *packet) { Enter_Method("addPacket"); + auto ownerQueue = dynamic_cast(packet->getOwner()); + if (ownerQueue != nullptr && dynamic_cast(ownerQueue->getOwner()) == nullptr) + throw cRuntimeError("Cannot buffer packet owned by cPacketQueue whose owner does not implement IPacketBuffer::ICallback"); EV_INFO << "Adding packet" << EV_FIELD(packet) << EV_ENDL; emit(packetAddedSignal, packet); packets.push_back(packet); if (isOverloaded()) { if (packetDropperFunction != nullptr) { + std::vector> droppedPackets; while (!isEmpty() && isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; packets.erase(find(packets, packet)); auto queue = dynamic_cast(packet->getOwner()); - if (queue != nullptr) { - ICallback *callback = dynamic_cast(queue->getOwner()); - if (callback != nullptr) - callback->handlePacketRemoved(packet); - } + auto callback = queue != nullptr ? check_and_cast(queue->getOwner()) : nullptr; + droppedPackets.emplace_back(packet, callback); + } + for (auto& [packet, callback] : droppedPackets) + if (callback != nullptr) + callback->handlePacketDropping(packet); + for (auto& [packet, callback] : droppedPackets) { + if (callback != nullptr) + callback->handlePacketDropped(packet); // TODO maybe the buffer should take ownership and queues should be aware of it take(packet); dropPacket(packet, QUEUE_OVERFLOW); diff --git a/src/inet/queueing/contract/IPacketBuffer.h b/src/inet/queueing/contract/IPacketBuffer.h index ced7f9556f0..0b177c7a976 100644 --- a/src/inet/queueing/contract/IPacketBuffer.h +++ b/src/inet/queueing/contract/IPacketBuffer.h @@ -27,6 +27,18 @@ class INET_API IPacketBuffer : public virtual IPacketCollection * The packet is never nullptr. */ virtual void handlePacketRemoved(Packet *packet) = 0; + + /** + * Detaches a packet selected for an overload drop from its owner before + * drop observers are notified. + */ + virtual void handlePacketDropping(Packet *packet) = 0; + + /** + * Notifies the packet owner after all packets selected by one overload + * operation have been removed from their owners. + */ + virtual void handlePacketDropped(Packet *packet) = 0; }; public: diff --git a/src/inet/queueing/contract/IPacketExtractor.h b/src/inet/queueing/contract/IPacketExtractor.h new file mode 100644 index 00000000000..cf2287ceeed --- /dev/null +++ b/src/inet/queueing/contract/IPacketExtractor.h @@ -0,0 +1,38 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IPACKETEXTRACTOR_H +#define __INET_IPACKETEXTRACTOR_H + +#include + +#include "inet/common/packet/Packet.h" + +namespace inet { +namespace queueing { + +/** Provides semantic pull accounting while extracting a selected packet. */ +class INET_API IPacketExtractor +{ + public: + /** + * Predicates must be stable and side-effect free from an initial + * findPacket() through the corresponding dequeuePacket() selection. + * Composite extractors may evaluate the predicate multiple times and on + * multiple candidates while preserving their scheduling policy. + */ + using PacketPredicate = std::function; + + public: + virtual ~IPacketExtractor() {} + virtual Packet *findPacket(const PacketPredicate& predicate) const = 0; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) = 0; +}; + +} // namespace queueing +} // namespace inet + +#endif diff --git a/src/inet/queueing/contract/IPacketQueue.cc b/src/inet/queueing/contract/IPacketQueue.cc new file mode 100644 index 00000000000..0be875f6df5 --- /dev/null +++ b/src/inet/queueing/contract/IPacketQueue.cc @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace queueing { + +simsignal_t IPacketQueue::packetQueueDepartureSignal = cComponent::registerSignal("packetQueueDeparture"); + +} // namespace queueing +} // namespace inet diff --git a/src/inet/queueing/contract/IPacketQueue.h b/src/inet/queueing/contract/IPacketQueue.h index 159fbb9fb91..da3da4c046d 100644 --- a/src/inet/queueing/contract/IPacketQueue.h +++ b/src/inet/queueing/contract/IPacketQueue.h @@ -9,6 +9,7 @@ #define __INET_IPACKETQUEUE_H #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" #include "inet/queueing/contract/IPassivePacketSink.h" #include "inet/queueing/contract/IPassivePacketSource.h" @@ -18,8 +19,22 @@ namespace queueing { /** * This class defines the interface for packet queues. */ -class INET_API IPacketQueue : public virtual IPacketCollection, public virtual IPassivePacketSink, public virtual IPassivePacketSource +class INET_API IPacketQueue : public virtual IPacketCollection, public virtual IPacketExtractor, public virtual IPassivePacketSink, public virtual IPassivePacketSource { + public: + enum class PacketRemovalReason { + DEQUEUED = 0, // Normal queue processing transferred ownership out of the queue. + REMOVED = 1, // Explicit removal outside normal queue processing. + DROPPED = 2, // The queue destructively removed the packet. + }; + + /** + * Emitted once per logical departure, after detachment and before transfer/deletion. + * The Packet and PacketQueueRemovalDetails are borrowed for synchronous delivery. + * Listeners must filter the source to their subscribed logical queue. + */ + static simsignal_t packetQueueDepartureSignal; + public: /** * Enqueues the packet into the packet queue. The onwership of the packet @@ -36,6 +51,14 @@ class INET_API IPacketQueue : public virtual IPacketCollection, public virtual I * The queue must not be empty. The returned packet must not be nullptr. */ virtual Packet *dequeuePacket() = 0; + + virtual Packet *findPacket(const PacketPredicate& predicate) const = 0; + + /** + * Dequeues the first matching packet according to the queue provider's + * scheduling policy. Ownership is transferred to the caller. + */ + virtual Packet *dequeuePacket(const PacketPredicate& predicate) = 0; }; } // namespace queueing diff --git a/src/inet/queueing/contract/IPacketQueue.ned b/src/inet/queueing/contract/IPacketQueue.ned index 40120433323..2911872be9d 100644 --- a/src/inet/queueing/contract/IPacketQueue.ned +++ b/src/inet/queueing/contract/IPacketQueue.ned @@ -16,6 +16,7 @@ package inet.queueing.contract; moduleinterface IPacketQueue extends IPassivePacketSink, IPassivePacketSource { parameters: + @signal[packetQueueDeparture](type=inet::Packet); // Details: PacketQueueRemovalDetails; source identifies the logical queue @omittedTypename(OmittedPacketQueue); @display("i=block/queue"); } diff --git a/src/inet/queueing/contract/PacketQueueRemovalDetails.h b/src/inet/queueing/contract/PacketQueueRemovalDetails.h new file mode 100644 index 00000000000..12989ba45bb --- /dev/null +++ b/src/inet/queueing/contract/PacketQueueRemovalDetails.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_PACKETQUEUEREMOVALDETAILS_H +#define __INET_PACKETQUEUEREMOVALDETAILS_H + +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace queueing { + +class INET_API PacketQueueRemovalDetails : public cObject +{ + protected: + IPacketQueue::PacketRemovalReason reason; + + public: + explicit PacketQueueRemovalDetails(IPacketQueue::PacketRemovalReason reason) : reason(reason) {} + IPacketQueue::PacketRemovalReason getReason() const { return reason; } +}; + +} // namespace queueing +} // namespace inet + +#endif diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.cc b/src/inet/queueing/queue/CompoundPacketQueueBase.cc index 7d290d0cc1e..b89e9620aad 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.cc +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.cc @@ -7,6 +7,9 @@ #include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include "inet/queueing/contract/IPacketBuffer.h" + #include "inet/common/Simsignals.h" namespace inet { @@ -14,6 +17,22 @@ namespace queueing { Define_Module(CompoundPacketQueueBase); +class ScopedPacketRemoval +{ + protected: + Packet *&packetBeingRemoved; + Packet *previousPacket; + + public: + ScopedPacketRemoval(Packet *&packetBeingRemoved, Packet *packet) : + packetBeingRemoved(packetBeingRemoved), previousPacket(packetBeingRemoved) + { + packetBeingRemoved = packet; + } + + ~ScopedPacketRemoval() { packetBeingRemoved = previousPacket; } +}; + void CompoundPacketQueueBase::initialize(int stage) { PacketQueueBase::initialize(stage); @@ -23,6 +42,11 @@ void CompoundPacketQueueBase::initialize(int stage) consumer.reference(inputGate, true, 1); provider.reference(outputGate, true, -1); collection = check_and_cast(provider.get()); + packetExtractor = check_and_cast(provider.get()); + // Observe the nearest queue on every descendant branch. Nested + // compound queues forward their own frontier, so stopping at a queue + // avoids duplicate notifications while traversing non-queue wrappers. + registerQueueFrontier(this); packetDropperFunction = createDropperFunction(par("dropperClass")); subscribe(packetDroppedSignal, this); subscribe(packetCreatedSignal, this); @@ -34,6 +58,39 @@ void CompoundPacketQueueBase::initialize(int stage) } } +void CompoundPacketQueueBase::registerQueueFrontier(cModule *module) +{ + for (cModule::SubmoduleIterator it(module); !it.end(); it++) { + auto childModule = *it; + auto childQueue = dynamic_cast(childModule); + if (childQueue != nullptr) { + childQueues.push_back(childQueue); + childModule->subscribe(IPacketQueue::packetQueueDepartureSignal, this); + } + else + registerQueueFrontier(childModule); + } +} + +void CompoundPacketQueueBase::finish() +{ + unsubscribeChildQueues(); + PacketQueueBase::finish(); +} + +void CompoundPacketQueueBase::preDelete(cComponent *root) +{ + unsubscribeChildQueues(); + PacketQueueBase::preDelete(root); +} + +void CompoundPacketQueueBase::unsubscribeChildQueues() +{ + for (auto childQueue : childQueues) + check_and_cast(childQueue)->unsubscribe(IPacketQueue::packetQueueDepartureSignal, this); + childQueues.clear(); +} + IPacketDropperFunction *CompoundPacketQueueBase::createDropperFunction(const char *dropperClass) const { if (strlen(dropperClass) == 0) @@ -58,11 +115,20 @@ void CompoundPacketQueueBase::pushPacket(Packet *packet, const cGate *gate) EV_INFO << "Pushing packet" << EV_FIELD(packet) << EV_ENDL; consumer.pushPacket(packet); if (packetDropperFunction != nullptr) { + std::vector droppedPackets; while (isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; - removePacket(packet); + { + ScopedPacketRemoval scopedPacketRemoval(packetBeingRemoved, packet); + collection->removePacket(packet); + } + emit(packetRemovedSignal, packet); take(packet); + droppedPackets.push_back(packet); + } + for (auto packet : droppedPackets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); dropPacket(packet, QUEUE_OVERFLOW); } } @@ -76,6 +142,7 @@ Packet *CompoundPacketQueueBase::pullPacket(const cGate *gate) Enter_Method("pullPacket"); auto packet = provider.pullPacket(); take(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); emit(packetPulledSignal, packet); return packet; } @@ -83,14 +150,43 @@ Packet *CompoundPacketQueueBase::pullPacket(const cGate *gate) void CompoundPacketQueueBase::removePacket(Packet *packet) { Enter_Method("removePacket"); - collection->removePacket(packet); + { + ScopedPacketRemoval scopedPacketRemoval(packetBeingRemoved, packet); + collection->removePacket(packet); + } + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); } +Packet *CompoundPacketQueueBase::findPacket(const PacketPredicate& predicate) const +{ + return packetExtractor->findPacket(predicate); +} + +Packet *CompoundPacketQueueBase::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + auto packet = packetExtractor->dequeuePacket(predicate); + if (packet == nullptr) + return nullptr; + take(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + // The owning leaf/provider has already recorded queue residence. The + // compound boundary mirrors pullPacket() and emits only its pull event. + emit(packetPulledSignal, packet); + drop(packet); + return packet; +} + void CompoundPacketQueueBase::removeAllPackets() { Enter_Method("removeAllPacket"); - collection->removeAllPackets(); + while (getNumPackets() != 0) { + auto packet = getPacket(0); + removePacket(packet); + take(packet); + delete packet; + } } bool CompoundPacketQueueBase::canPushSomePacket(const cGate *gate) const @@ -118,14 +214,38 @@ bool CompoundPacketQueueBase::canPushPacket(Packet *packet, const cGate *gate) c void CompoundPacketQueueBase::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signal)); - if (signal == packetDroppedSignal) + if (signal == IPacketQueue::packetQueueDepartureSignal) { + if (source != this && source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } + else if (signal == packetDroppedSignal) { numDroppedPackets++; + // Child queues already publish departures, and shared buffers notify + // the owning queue before dropping. Only translate drops outside those + // boundaries, such as input filters rejecting a packet before enqueue. + auto module = dynamic_cast(source); + while (module != nullptr && module != this) { + if (dynamic_cast(module) != nullptr || dynamic_cast(module) != nullptr) + break; + module = module->getParentModule(); + } + if (source != this && module == this) + notifyPacketRemoved(check_and_cast(object), IPacketQueue::PacketRemovalReason::DROPPED); + } else if (signal == packetCreatedSignal) numCreatedPackets++; else throw cRuntimeError("Unknown signal"); } +void CompoundPacketQueueBase::handlePacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == IPacketQueue::PacketRemovalReason::DROPPED || + (reason == IPacketQueue::PacketRemovalReason::REMOVED && packet != packetBeingRemoved)) + notifyPacketRemoved(packet, reason); +} + } // namespace queueing } // namespace inet diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.h b/src/inet/queueing/queue/CompoundPacketQueueBase.h index 706d9fb32c5..c89072bdf10 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.h +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.h @@ -26,17 +26,28 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene PassivePacketSinkRef consumer; PassivePacketSourceRef provider; IPacketCollection *collection = nullptr; + IPacketExtractor *packetExtractor = nullptr; + std::vector childQueues; + Packet *packetBeingRemoved = nullptr; IPacketDropperFunction *packetDropperFunction = nullptr; protected: + using cListener::finish; + virtual void initialize(int stage) override; + virtual void finish() override; + virtual void preDelete(cComponent *root) override; + virtual void registerQueueFrontier(cModule *module); + virtual void unsubscribeChildQueues(); virtual IPacketDropperFunction *createDropperFunction(const char *dropperClass) const; virtual bool isOverloaded() const; public: + using PacketQueueBase::dequeuePacket; + virtual ~CompoundPacketQueueBase() { delete packetDropperFunction; } virtual int getMaxNumPackets() const override { return packetCapacity; } @@ -48,6 +59,8 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene virtual bool isEmpty() const override { return collection->isEmpty(); } virtual Packet *getPacket(int index) const override { return collection->getPacket(index); } virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual bool supportsPacketPushing(const cGate *gate) const override { return inputGate == gate; } @@ -61,6 +74,7 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene virtual Packet *pullPacket(const cGate *gate) override; virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason); }; } // namespace queueing diff --git a/src/inet/queueing/queue/PacketQueue.cc b/src/inet/queueing/queue/PacketQueue.cc index 5cb89c3a638..567c45a0e31 100644 --- a/src/inet/queueing/queue/PacketQueue.cc +++ b/src/inet/queueing/queue/PacketQueue.cc @@ -8,9 +8,7 @@ #include "inet/queueing/queue/PacketQueue.h" #include "inet/common/ModuleAccess.h" -#include "inet/common/PacketEventTag.h" #include "inet/common/Simsignals.h" -#include "inet/common/TimeTag.h" #include "inet/queueing/function/PacketComparatorFunction.h" #include "inet/queueing/function/PacketDropperFunction.h" @@ -100,10 +98,15 @@ void PacketQueue::pushPacket(Packet *packet, const cGate *gate) throw cRuntimeError("Queue is overloaded while using a packet buffer"); } else if (packetDropperFunction != nullptr) { + std::vector droppedPackets; while (isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; queue.remove(packet); + droppedPackets.push_back(packet); + } + for (auto packet : droppedPackets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); dropPacket(packet, QUEUE_OVERFLOW); } } @@ -126,13 +129,38 @@ Packet *PacketQueue::pullPacket(const cGate *gate) } else queue.pop(); - auto queueingTime = simTime() - packet->getArrivalTime(); - auto packetEvent = new PacketEvent(); - insertPacketEvent(this, packet, PEK_QUEUED, 0, queueingTime, packetEvent); - increaseTimeTag(packet, queueingTime, queueingTime); - emit(packetPulledSignal, packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + recordPacketDequeued(packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + return packet; +} + +Packet *PacketQueue::findPacket(const PacketPredicate& predicate) const +{ + for (int i = 0; i < queue.getLength(); i++) { + auto packet = check_and_cast(queue.get(i)); + if (predicate(packet)) + return packet; + } + return nullptr; +} + +Packet *PacketQueue::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + auto packet = findPacket(predicate); + if (packet == nullptr) + return nullptr; + EV_INFO << "Dequeuing packet" << EV_FIELD(packet) << EV_ENDL; + queue.remove(packet); + if (buffer != nullptr) + buffer->removePacket(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + recordPacketDequeued(packet); if (collector != nullptr) animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); return packet; } @@ -143,6 +171,7 @@ void PacketQueue::removePacket(Packet *packet) queue.remove(packet); if (buffer != nullptr) buffer->removePacket(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); } @@ -151,11 +180,13 @@ void PacketQueue::removeAllPackets() Enter_Method("removeAllPackets"); EV_INFO << "Removing all packets" << EV_ENDL; std::vector packets; - for (int i = 0; i < getNumPackets(); i++) + while (!queue.isEmpty()) packets.push_back(check_and_cast(queue.pop())); if (buffer != nullptr) - buffer->removeAllPackets(); + for (auto packet : packets) + buffer->removePacket(packet); for (auto packet : packets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); delete packet; } @@ -190,9 +221,26 @@ void PacketQueue::handlePacketRemoved(Packet *packet) EV_INFO << "Removing packet" << EV_FIELD(packet) << EV_ENDL; queue.remove(packet); emit(packetRemovedSignal, packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); } } +void PacketQueue::handlePacketDropping(Packet *packet) +{ + Enter_Method("handlePacketDropping"); + if (queue.contains(packet)) { + EV_INFO << "Removing packet" << EV_FIELD(packet) << EV_ENDL; + queue.remove(packet); + emit(packetRemovedSignal, packet); + } +} + +void PacketQueue::handlePacketDropped(Packet *packet) +{ + Enter_Method("handlePacketDropped"); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); +} + } // namespace queueing } // namespace inet diff --git a/src/inet/queueing/queue/PacketQueue.h b/src/inet/queueing/queue/PacketQueue.h index 4debf8541ec..5a15942d20b 100644 --- a/src/inet/queueing/queue/PacketQueue.h +++ b/src/inet/queueing/queue/PacketQueue.h @@ -45,6 +45,8 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual bool isOverloaded() const; public: + using PacketQueueBase::dequeuePacket; + virtual ~PacketQueue() { delete packetDropperFunction; } virtual cGate *getRegistrationForwardingGate(cGate *gate) override; @@ -58,6 +60,8 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual bool supportsPacketPushing(const cGate *gate) const override { return inputGate == gate; } @@ -71,6 +75,8 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual Packet *pullPacket(const cGate *gate) override; virtual void handlePacketRemoved(Packet *packet) override; + virtual void handlePacketDropping(Packet *packet) override; + virtual void handlePacketDropped(Packet *packet) override; }; } // namespace queueing diff --git a/src/inet/queueing/scheduler/LabelScheduler.cc b/src/inet/queueing/scheduler/LabelScheduler.cc index af30138c225..1e5646ba50a 100644 --- a/src/inet/queueing/scheduler/LabelScheduler.cc +++ b/src/inet/queueing/scheduler/LabelScheduler.cc @@ -20,40 +20,130 @@ void LabelScheduler::initialize(int stage) if (stage == INITSTAGE_LOCAL) { defaultGateIndex = par("defaultGateIndex"); labels = cStringTokenizer(par("labels")).asVector(); - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + auto collection = dynamic_cast(provider); + auto packetExtractor = dynamic_cast(provider); + collections.push_back(collection); + packetExtractors.push_back(packetExtractor); + } } } int LabelScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); size += collection->getNumPackets(); + } return size; } b LabelScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); totalLength += collection->getTotalLength(); + } return totalLength; } Packet *LabelScheduler::getPacket(int index) const { - throw cRuntimeError("TODO"); + int originalIndex = index; + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + auto numPackets = collection->getNumPackets(); + if (index < numPackets) + return collection->getPacket(index); + index -= numPackets; + } + throw cRuntimeError("Index %i out of range", originalIndex); } void LabelScheduler::removePacket(Packet *packet) { - throw cRuntimeError("TODO"); + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + for (int i = 0; i < collection->getNumPackets(); i++) { + if (collection->getPacket(i) == packet) { + collection->removePacket(packet); + return; + } + } + } + throw cRuntimeError("Cannot find packet"); +} + +int LabelScheduler::findInput(const PacketPredicate& predicate) const +{ + std::vector candidates; + for (size_t i = 0; i < packetExtractors.size(); i++) { + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + candidates.push_back(packetExtractors[i]->findPacket(predicate)); + } + for (auto label : labels) { + for (size_t i = 0; i < candidates.size(); i++) { + auto packet = candidates[i]; + if (packet == nullptr) + continue; + const auto& labelsTag = packet->findTag(); + if (labelsTag != nullptr) { + for (size_t j = 0; j < labelsTag->getLabelsArraySize(); j++) + if (label == labelsTag->getLabels(j)) + return i; + } + } + } + return defaultGateIndex >= 0 && defaultGateIndex < (int)candidates.size() && candidates[defaultGateIndex] != nullptr ? defaultGateIndex : -1; +} + +Packet *LabelScheduler::findPacket(const PacketPredicate& predicate) const +{ + auto index = findInput(predicate); + return index == -1 ? nullptr : packetExtractors[index]->findPacket(predicate); +} + +Packet *LabelScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + if (index == -1) + return nullptr; + auto packet = packetExtractors[index]->dequeuePacket(predicate); + ASSERT(packet != nullptr); + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; } void LabelScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } diff --git a/src/inet/queueing/scheduler/LabelScheduler.h b/src/inet/queueing/scheduler/LabelScheduler.h index bb03cd04af6..c808139edab 100644 --- a/src/inet/queueing/scheduler/LabelScheduler.h +++ b/src/inet/queueing/scheduler/LabelScheduler.h @@ -10,20 +10,23 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: int defaultGateIndex = -1; std::vector labels; std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; virtual int schedulePacket() override; + virtual int findInput(const PacketPredicate& predicate) const; public: virtual int getMaxNumPackets() const override { return -1; } @@ -35,6 +38,8 @@ class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPack virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; }; diff --git a/src/inet/queueing/scheduler/PriorityScheduler.cc b/src/inet/queueing/scheduler/PriorityScheduler.cc index 346d2b313c2..1bbeaffcef1 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.cc +++ b/src/inet/queueing/scheduler/PriorityScheduler.cc @@ -16,37 +16,47 @@ void PriorityScheduler::initialize(int stage) { PacketSchedulerBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + collections.push_back(dynamic_cast(provider)); + auto packetExtractor = dynamic_cast(provider); + packetExtractors.push_back(packetExtractor); + } } } int PriorityScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) - if (collection != nullptr) - size += collection->getNumPackets(); - else - return -1; + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + size += collection->getNumPackets(); + } return size; } b PriorityScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) - if (collection != nullptr) - totalLength += collection->getTotalLength(); - else - return b(-1); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); + totalLength += collection->getTotalLength(); + } return totalLength; } Packet *PriorityScheduler::getPacket(int index) const { int origIndex = index; - for (auto collection : collections) { + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; auto numPackets = collection->getNumPackets(); if (index < numPackets) return collection->getPacket(index); @@ -59,7 +69,11 @@ Packet *PriorityScheduler::getPacket(int index) const void PriorityScheduler::removePacket(Packet *packet) { Enter_Method("removePacket"); - for (auto collection : collections) { + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; int numPackets = collection->getNumPackets(); for (int j = 0; j < numPackets; j++) { if (collection->getPacket(j) == packet) { @@ -71,9 +85,53 @@ void PriorityScheduler::removePacket(Packet *packet) throw cRuntimeError("Cannot find packet"); } +Packet *PriorityScheduler::findPacket(const PacketPredicate& predicate) const +{ + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto index = reverseOrder ? collections.size() - i - 1 : i; + if (packetExtractors[index] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)index); + auto packet = packetExtractors[index]->findPacket(predicate); + if (packet != nullptr) + return packet; + } + return nullptr; +} + +Packet *PriorityScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto index = reverseOrder ? collections.size() - i - 1 : i; + if (packetExtractors[index] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)index); + auto packet = packetExtractors[index]->dequeuePacket(predicate); + if (packet == nullptr) + continue; + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; + } + return nullptr; +} + void PriorityScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } diff --git a/src/inet/queueing/scheduler/PriorityScheduler.h b/src/inet/queueing/scheduler/PriorityScheduler.h index 09d6af3ffb9..173179ac3d9 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.h +++ b/src/inet/queueing/scheduler/PriorityScheduler.h @@ -10,14 +10,16 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; @@ -33,6 +35,8 @@ class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IP virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual void handleCanPullPacketChanged(const cGate *gate) override; diff --git a/src/inet/queueing/scheduler/WrrScheduler.cc b/src/inet/queueing/scheduler/WrrScheduler.cc index a551bb69526..d52d84ff80a 100644 --- a/src/inet/queueing/scheduler/WrrScheduler.cc +++ b/src/inet/queueing/scheduler/WrrScheduler.cc @@ -37,30 +37,141 @@ void WrrScheduler::initialize(int stage) if (tokenizer.hasMoreTokens()) throw cRuntimeError("Too many values given in the weights parameter."); - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + auto collection = dynamic_cast(provider); + auto packetExtractor = dynamic_cast(provider); + collections.push_back(collection); + packetExtractors.push_back(packetExtractor); + } } } int WrrScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); size += collection->getNumPackets(); + } return size; } b WrrScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); totalLength += collection->getTotalLength(); + } return totalLength; } +Packet *WrrScheduler::getPacket(int index) const +{ + int originalIndex = index; + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + auto numPackets = collection->getNumPackets(); + if (index < numPackets) + return collection->getPacket(index); + index -= numPackets; + } + throw cRuntimeError("Index %i out of range", originalIndex); +} + +void WrrScheduler::removePacket(Packet *packet) +{ + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + for (int i = 0; i < collection->getNumPackets(); i++) { + if (collection->getPacket(i) == packet) { + collection->removePacket(packet); + return; + } + } + } + throw cRuntimeError("Cannot find packet"); +} + +int WrrScheduler::findInput(const PacketPredicate& predicate) const +{ + int firstWeighted = -1; + int firstNonWeighted = -1; + for (size_t i = 0; i < collections.size(); ++i) { + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + if (packetExtractors[i]->findPacket(predicate) != nullptr) { + if (buckets[i] > 0) + return i; + else if (firstWeighted == -1 && weights[i] > 0) + firstWeighted = i; + else if (firstNonWeighted == -1 && weights[i] == 0) + firstNonWeighted = i; + } + } + return firstWeighted != -1 ? firstWeighted : firstNonWeighted; +} + +void WrrScheduler::consumeBucket(int index) +{ + if (weights[index] == 0) + return; + if (buckets[index] == 0) { + for (size_t i = 0; i < collections.size(); ++i) + buckets[i] = weights[i]; + } + ASSERT(buckets[index] > 0); + buckets[index]--; +} + +Packet *WrrScheduler::findPacket(const PacketPredicate& predicate) const +{ + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + return index == -1 ? nullptr : packetExtractors[index]->findPacket(predicate); +} + +Packet *WrrScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + if (index == -1) + return nullptr; + auto packet = packetExtractors[index]->dequeuePacket(predicate); + ASSERT(packet != nullptr); + consumeBucket(index); + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + void WrrScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } diff --git a/src/inet/queueing/scheduler/WrrScheduler.h b/src/inet/queueing/scheduler/WrrScheduler.h index d14f19b6b78..bc161fb4b3a 100644 --- a/src/inet/queueing/scheduler/WrrScheduler.h +++ b/src/inet/queueing/scheduler/WrrScheduler.h @@ -10,6 +10,7 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { @@ -17,17 +18,20 @@ namespace queueing { /** * This module implements a Weighted Round Robin Scheduler. */ -class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: unsigned int *weights = nullptr; // array of weights (has numInputs elements) unsigned int *buckets = nullptr; // array of tokens in buckets (has numInputs elements) std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; virtual int schedulePacket() override; + virtual int findInput(const PacketPredicate& predicate) const; + virtual void consumeBucket(int index); public: virtual ~WrrScheduler(); @@ -39,8 +43,10 @@ class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacket virtual b getTotalLength() const override; virtual bool isEmpty() const override { return getNumPackets() == 0; } - virtual Packet *getPacket(int index) const override { throw cRuntimeError("Invalid operation"); } - virtual void removePacket(Packet *packet) override { throw cRuntimeError("Invalid operation"); } + virtual Packet *getPacket(int index) const override; + virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; }; diff --git a/tests/queueing/CompoundPacketQueueCompositionDeparture_1.test b/tests/queueing/CompoundPacketQueueCompositionDeparture_1.test new file mode 100644 index 00000000000..6fdc46437fc --- /dev/null +++ b/tests/queueing/CompoundPacketQueueCompositionDeparture_1.test @@ -0,0 +1,122 @@ +%description: +Verify exactly one departure at each exposed nested compound queue boundary for +input RED rejection, leaf overflow, shared-buffer eviction, selective extraction, +ordinary dequeue and explicit removal. + +%includes: +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + +%global: +using namespace inet; +using namespace inet::queueing; +class BoundaryObserver : public cListener { + public: + cComponent *boundary = nullptr; + int drops = 0; + int dequeues = 0; + int removals = 0; + long droppedId = -1; + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *object, cObject *details) override { + if (source != boundary) + return; + auto reason = check_and_cast(details)->getReason(); + auto packet = check_and_cast(object); + if (reason == IPacketQueue::PacketRemovalReason::DROPPED) { + drops++; + droppedId = packet->getId(); + } + else if (reason == IPacketQueue::PacketRemovalReason::DEQUEUED) + dequeues++; + else if (reason == IPacketQueue::PacketRemovalReason::REMOVED) + removals++; + } +}; + +%activity: +for (const auto type : {"NestedRedDepartureQueue", "NestedLeafDepartureQueue", "NestedBufferDepartureQueue"}) { + auto module = cModuleType::get(type)->create("queue", this); + module->finalizeParameters(); + module->buildInside(); + module->callInitialize(); + auto inner = module->getSubmodule("inner"); + BoundaryObserver outerObserver, innerObserver; + outerObserver.boundary = module; + innerObserver.boundary = inner; + module->subscribe(IPacketQueue::packetQueueDepartureSignal, &outerObserver); + inner->subscribe(IPacketQueue::packetQueueDepartureSignal, &innerObserver); + auto queue = check_and_cast(module); + auto makePacket = []() { return new Packet("packet", makeShared(B(10))); }; + queue->enqueuePacket(makePacket()); + auto rejected = makePacket(); + auto rejectedId = rejected->getId(); + queue->enqueuePacket(rejected); + ASSERT(queue->getNumPackets() == 1); + ASSERT(outerObserver.drops == 1 && innerObserver.drops == 1); + ASSERT(outerObserver.droppedId == rejectedId && innerObserver.droppedId == rejectedId); + auto selected = queue->dequeuePacket([](const Packet *) { return true; }); + take(selected); + delete selected; + wait(SimTime(1, SIMTIME_US)); + ASSERT(outerObserver.dequeues == 1 && innerObserver.dequeues == 1); + queue->enqueuePacket(makePacket()); + auto dequeued = queue->dequeuePacket(); + take(dequeued); + delete dequeued; + wait(SimTime(1, SIMTIME_US)); + ASSERT(outerObserver.dequeues == 2 && innerObserver.dequeues == 2); + queue->enqueuePacket(makePacket()); + auto removed = queue->getPacket(0); + queue->removePacket(removed); + take(removed); + delete removed; + ASSERT(outerObserver.removals == 1 && innerObserver.removals == 1); + ASSERT(outerObserver.drops == 1 && innerObserver.drops == 1); + module->unsubscribe(IPacketQueue::packetQueueDepartureSignal, &outerObserver); + inner->unsubscribe(IPacketQueue::packetQueueDepartureSignal, &innerObserver); + module->deleteModule(); +} + +%file: queues.ned +import inet.queueing.queue.CompoundPacketQueueBase; +import inet.queueing.queue.RedDropperQueue; +import inet.queueing.queue.PacketQueue; +import inet.queueing.contract.IPacketQueue; +import inet.queueing.buffer.PacketBuffer; +module CompositionRedQueue extends RedDropperQueue { + parameters: + @class(::inet::queueing::CompoundPacketQueueBase); + red.minth = 0; + red.maxth = 1; + red.packetCapacity = 1; + red.wq = 1; + red.maxp = 0; +} +module NestedDepartureQueue extends CompoundPacketQueueBase { + parameters: + @class(::inet::queueing::CompoundPacketQueueBase); + bool useBuffer = default(false); + submodules: + buffer: PacketBuffer if useBuffer { + packetCapacity = 1; + dropperClass = "inet::queueing::PacketAtCollectionEndDropper"; + } + inner: like IPacketQueue; + connections: + in --> inner.in; + inner.out --> out; +} +module NestedRedDepartureQueue extends NestedDepartureQueue { + parameters: + inner.typename = "CompositionRedQueue"; +} +module NestedLeafDepartureQueue extends NestedDepartureQueue { + parameters: + inner.packetCapacity = 1; + inner.dropperClass = "inet::queueing::PacketAtCollectionEndDropper"; +} +module NestedBufferDepartureQueue extends NestedDepartureQueue { + parameters: + useBuffer = true; + inner.bufferModule = "^.buffer"; +} diff --git a/tests/queueing/CompoundPacketQueueRedDeparture_1.test b/tests/queueing/CompoundPacketQueueRedDeparture_1.test new file mode 100644 index 00000000000..8682a4c7c1b --- /dev/null +++ b/tests/queueing/CompoundPacketQueueRedDeparture_1.test @@ -0,0 +1,54 @@ +%description: +A RED rejection at a compound queue boundary must publish a logical drop. +%includes: +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/queueing/contract/IPacketQueue.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +%global: +using namespace inet; +class DepartureProbe : public cListener { + public: + long droppedId = -1; + long departedId = -1; + int drops = 0; + int departures = 0; + virtual void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == packetDroppedSignal) { drops++; droppedId = check_and_cast(object)->getId(); } + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + departures++; departedId = check_and_cast(object)->getId(); + ASSERT(check_and_cast(details)->getReason() == queueing::IPacketQueue::PacketRemovalReason::DROPPED); + } + } +}; +%activity: +auto module = cModuleType::get("DepartureRedQueue")->create("redProbe", this); +module->finalizeParameters(); +module->buildInside(); +module->callInitialize(); +DepartureProbe listener; +module->subscribe(packetDroppedSignal, &listener); +module->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &listener); +auto queue = check_and_cast(module); +queue->enqueuePacket(new Packet("first", makeShared(B(10)))); +queue->enqueuePacket(new Packet("second", makeShared(B(10)))); +EV_WARN << "RED probe: dropped=" << listener.drops << " logicalDepartures=" << listener.departures << endl; +ASSERT(listener.drops == 1); +ASSERT(listener.departures == 1); +ASSERT(listener.droppedId == listener.departedId); +module->unsubscribe(packetDroppedSignal, &listener); +module->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &listener); +module->deleteModule(); + +%file: redprobe.ned +import inet.queueing.queue.RedDropperQueue; +module DepartureRedQueue extends RedDropperQueue +{ + parameters: + @class(::inet::queueing::CompoundPacketQueueBase); + red.minth = 0; + red.maxth = 1; + red.packetCapacity = 1; + red.wq = 1; + red.maxp = 0; +} diff --git a/tests/queueing/PacketQueueDepartureSignal_1.test b/tests/queueing/PacketQueueDepartureSignal_1.test new file mode 100644 index 00000000000..20df6b35c0c --- /dev/null +++ b/tests/queueing/PacketQueueDepartureSignal_1.test @@ -0,0 +1,98 @@ +%description: +Verify that a PacketQueue using an external PacketBuffer reports one typed +departure signal while the evicted packet is still valid, before the buffer +deletes it. + +%file: TestPacketQueueDepartureSignal.cc + +#include + +#include "inet/common/packet/Packet.h" +#include "inet/queueing/buffer/PacketBuffer.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include "inet/queueing/queue/PacketQueue.h" + +namespace inet { +namespace queueing { + +class TestPacketQueueDepartureSignal : public cSimpleModule, public cListener +{ + public: + TestPacketQueueDepartureSignal() : cSimpleModule(65536) {} + + protected: + int notificationCount = 0; + std::string lastPacketName; + bool packetWasOwned = false; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override + { + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + ASSERT(reason == IPacketQueue::PacketRemovalReason::DROPPED); + notificationCount++; + lastPacketName = packet->getName(); + packetWasOwned = packet->getOwner() != nullptr; + } + + virtual void activity() override + { + auto queue = check_and_cast(getModuleByPath("^.queue")); + auto buffer = check_and_cast(getModuleByPath("^.buffer")); + queue->subscribe(IPacketQueue::packetQueueDepartureSignal, this); + ASSERT(queue->getMaxNumPackets() == -1); + ASSERT(buffer->getMaxNumPackets() == 0); + + auto packet = new Packet("external-buffer-packet"); + queue->enqueuePacket(packet); + + queue->unsubscribe(IPacketQueue::packetQueueDepartureSignal, this); + ASSERT(notificationCount == 1); + ASSERT(lastPacketName == "external-buffer-packet"); + ASSERT(packetWasOwned); + ASSERT(queue->isEmpty()); + ASSERT(buffer->isEmpty()); + std::cout << "External PacketBuffer eviction notified the queue signal exactly once before deletion.\n"; + } +}; + +Define_Module(TestPacketQueueDepartureSignal); + +} // namespace queueing +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.queueing.buffer.PacketBuffer; +import inet.queueing.queue.PacketQueue; + +simple TestPacketQueueDepartureSignal extends SimpleModule +{ + parameters: + @class(::inet::queueing::TestPacketQueueDepartureSignal); +} + +network PacketQueueDepartureSignalTestNetwork +{ + submodules: + buffer: PacketBuffer; + queue: PacketQueue; + test: TestPacketQueueDepartureSignal; +} + +%inifile: omnetpp.ini + +[General] +network = PacketQueueDepartureSignalTestNetwork +sim-time-limit = 1us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.queue.bufferModule = "^.buffer" +*.buffer.packetCapacity = 0 +*.buffer.dropperClass = "inet::queueing::PacketAtCollectionEndDropper" + +%contains: stdout +External PacketBuffer eviction notified the queue signal exactly once before deletion. From 546089a17c41a38b661e2ac1ab4bd2e0b1fdcc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:36:29 +0200 Subject: [PATCH 28/91] ieee80211: fix: track pending queue departures through signals DCF and HCF must retire pending-frame eligibility when the logical queue dequeues, removes or drops a frame. Subscribe to the complete queue departure contract and filter descendant emissions so each outcome is handled once. Validation: diagnostic debug and release builds; 8 focused unit/module/queueing cases; 56 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.change.fix | test | pr-1148-resolve-audit-findings --- .../ieee80211/mac/coordinationfunction/Dcf.cc | 29 +- .../ieee80211/mac/coordinationfunction/Dcf.h | 4 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 29 +- .../ieee80211/mac/coordinationfunction/Hcf.h | 4 +- .../module/Ieee80211MgmtApHcfQueueDrop_1.test | 6 +- tests/module/Ieee80211MgmtApQueueDrop_1.test | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 947 ++++++++++++++++++ 7 files changed, 995 insertions(+), 26 deletions(-) create mode 100644 tests/unit/Ieee80211AddbaTransaction_1.test diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index e286df0df07..85835ebd822 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -53,7 +55,7 @@ void Dcf::initialize(int stage) else if (stage == INITSTAGE_LAST) { // Dcaf resolves its pending queue at the link-layer stage. Install // this signal listener after all child initialization has completed. - check_and_cast(channelAccess->getPendingQueue())->subscribe(packetDroppedSignal, this); + check_and_cast(channelAccess->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } } @@ -121,16 +123,22 @@ void Dcf::processMgmtFrame(Packet *packet, const Ptr& throw cRuntimeError("Unknown management frame"); } -void Dcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void Dcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) { - if (signalID == packetDroppedSignal) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - auto packet = check_and_cast(obj); - if (packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); - } + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } +} + +void Dcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); } } @@ -427,4 +435,3 @@ Dcf::~Dcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 21908e37454..19995cb221a 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -28,6 +28,7 @@ #include "inet/linklayer/ieee80211/mac/originator/AckHandler.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.h" +#include "inet/queueing/contract/IPacketQueue.h" namespace inet { namespace ieee80211 { @@ -87,7 +88,6 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void initialize(int stage) override; virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void sendUp(const std::vector& completeFrames); virtual bool hasFrameToTransmit(); @@ -118,6 +118,8 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual bool isSentByUs(const Ptr& header) const; virtual bool isForUs(const Ptr& header) const; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index f4473f4b311..6b13cb7acb1 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -69,7 +71,7 @@ void Hcf::initialize(int stage) // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) - check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(packetDroppedSignal, this); + check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } } @@ -154,16 +156,22 @@ void Hcf::processUpperFrame(Packet *packet, const Ptr(obj); - if (packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); - } + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } +} + +void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); } } @@ -826,4 +834,3 @@ Hcf::~Hcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 22398f73831..10a7bc50c56 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -36,6 +36,7 @@ #include "inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/CtsProcedure.h" +#include "inet/queueing/contract/IPacketQueue.h" namespace inet { namespace ieee80211 { @@ -107,7 +108,6 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void initialize(int stage) override; virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); @@ -157,6 +157,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; diff --git a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test index 3f301e472f6..67c2368ec26 100644 --- a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test @@ -1,7 +1,7 @@ %description: Verify that a tagged Association or Reassociation Response dropped from an HCF EDCA compound pending queue reaches the AP as a terminal outcome. The -child management queue drop signal is propagated through the compound queue, +child management queue departure signal is propagated through the compound queue, pending state is cleared, and a retransmission completes without disturbing a committed association. @@ -23,6 +23,7 @@ class TestHcfManagementQueue : public queueing::PacketQueue { public: void setPacketCapacityForTest(int capacity) { packetCapacity = capacity; } + bool hasPacketDepartureListenerForTest() const { return !getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal).empty(); } }; Define_Module(TestHcfManagementQueue); @@ -31,6 +32,7 @@ class TestCompoundPendingQueueForHcf : public queueing::CompoundPacketQueueBase { public: void setPacketCapacityForTest(int capacity) { packetCapacity = capacity; } + bool hasPacketDepartureListenerForTest() const { return !getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal).empty(); } }; Define_Module(TestCompoundPendingQueueForHcf); @@ -129,6 +131,8 @@ class Ieee80211MgmtApHcfQueueDropTest : public cSimpleModule ASSERT(compoundQueue->getMaxNumPackets() == -1); ASSERT(managementQueue->getMaxNumPackets() == 0); + ASSERT(compoundQueue->hasPacketDepartureListenerForTest()); + ASSERT(managementQueue->hasPacketDepartureListenerForTest()); mgmt->markAuthenticated(station); mgmt->submitAssociationRequest(station); for (int i = 0; i < 100 && mgmt->statuses.empty(); i++) diff --git a/tests/module/Ieee80211MgmtApQueueDrop_1.test b/tests/module/Ieee80211MgmtApQueueDrop_1.test index 7a4444bd1ad..a428423bf9c 100644 --- a/tests/module/Ieee80211MgmtApQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApQueueDrop_1.test @@ -105,7 +105,7 @@ class Ieee80211MgmtApQueueDropTest : public cSimpleModule ASSERT(!mgmt->hasPendingAssociation(station)); ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::AUTHENTICATED); - // The existing packetDroppedSignal is delivered before deletion, so the reservation + // The queue removal notification is delivered before deletion, so the reservation // is released synchronously and can be reused by another STA. ASSERT(mgmt->reserveAssociationId(reusable) == 1); mgmt->cancelAssociationIdReservation(reusable); diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test new file mode 100644 index 00000000000..91a9667ac13 --- /dev/null +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -0,0 +1,947 @@ +%description: +Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DCF/HCF continuation, and queue accounting. + +%includes: +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include +#include + +#include "inet/common/PacketEventTag.h" +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/common/LabelsTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/queueing/gate/PacketGate.h" +#include "inet/queueing/gate/PeriodicGate.h" +#include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/queueing/queue/PacketQueue.h" +#include "inet/queueing/scheduler/LabelScheduler.h" +#include "inet/queueing/scheduler/PriorityScheduler.h" +#include "inet/queueing/scheduler/WrrScheduler.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; +class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue +{ + public: + virtual ~TestPacketQueue() { + for (auto listener : getLocalSignalListeners(packetQueueDepartureSignal)) + unsubscribe(packetQueueDepartureSignal, listener); + } + std::vector packets; + int numSelectedDequeues = 0; + mutable int numGetPacketCalls = 0; + + void notifyPacketRemoved(Packet *packet, PacketRemovalReason reason) { + queueing::PacketQueueRemovalDetails details(reason); + emit(queueing::IPacketQueue::packetQueueDepartureSignal, packet, &details); + } + + virtual int getMaxNumPackets() const override { return -1; } + virtual int getNumPackets() const override { return packets.size(); } + virtual b getMaxTotalLength() const override { return b(-1); } + virtual b getTotalLength() const override { return b(0); } + virtual Packet *getPacket(int index) const override { numGetPacketCalls++; return packets.at(index); } + virtual bool isEmpty() const override { return packets.empty(); } + virtual void removePacket(Packet *packet) override { + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::REMOVED); + } + virtual void removeAllPackets() override { + auto removedPackets = packets; + packets.clear(); + for (auto packet : removedPackets) + notifyPacketRemoved(packet, PacketRemovalReason::REMOVED); + } + virtual void enqueuePacket(Packet *packet) override { packets.push_back(packet); } + virtual Packet *dequeuePacket() override { + auto packet = packets.front(); + packets.erase(packets.begin()); + notifyPacketRemoved(packet, PacketRemovalReason::DEQUEUED); + return packet; + } + virtual Packet *findPacket(const PacketPredicate& predicate) const override { + auto it = std::find_if(packets.begin(), packets.end(), predicate); + return it == packets.end() ? nullptr : *it; + } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + auto packet = findPacket(predicate); + if (packet != nullptr) { + numSelectedDequeues++; + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::DEQUEUED); + } + return packet; + } + void dropPacketFromQueue(Packet *packet) { + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::DROPPED); + } + virtual bool canPullSomePacket(const cGate *) const override { return !packets.empty(); } + virtual Packet *canPullPacket(const cGate *) const override { return packets.empty() ? nullptr : packets.front(); } + virtual Packet *pullPacket(const cGate *) override { return dequeuePacket(); } + virtual Packet *pullPacketStart(const cGate *, bps) override { throw cRuntimeError("Unsupported"); } + virtual Packet *pullPacketEnd(const cGate *) override { throw cRuntimeError("Unsupported"); } + virtual Packet *pullPacketProgress(const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } + virtual bool canPushSomePacket(const cGate *) const override { return true; } + virtual bool canPushPacket(Packet *, const cGate *) const override { return true; } + virtual void pushPacket(Packet *packet, const cGate *) override { enqueuePacket(packet); } + virtual void pushPacketStart(Packet *, const cGate *, bps) override { throw cRuntimeError("Unsupported"); } + virtual void pushPacketEnd(Packet *, const cGate *) override { throw cRuntimeError("Unsupported"); } + virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } +}; +class TestCompoundPacketQueue : public queueing::CompoundPacketQueueBase +{ + public: + void configure(queueing::IPacketCollection *collection, queueing::IPacketExtractor *packetExtractor) { + this->collection = collection; + this->packetExtractor = packetExtractor; + } + void release(Packet *packet) { + take(packet); + drop(packet); + } + void observeDescendantQueues(cModule *module) { registerQueueFrontier(module); } + void observeQueue(queueing::IPacketQueue *queue) { childQueues.push_back(queue); check_and_cast(queue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } + void addObserver(cListener *callback) { subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, callback); } + void stopObserving() { + for (auto queue : childQueues) + check_and_cast(queue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + childQueues.clear(); + for (auto listener : getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal)) + unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, listener); + } +}; + +class TestPriorityScheduler : public queueing::PriorityScheduler +{ + public: + void configure(const std::vector& collections, bool reverseOrder = false) { + std::vector packetExtractors; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + configure(collections, packetExtractors, reverseOrder); + } + void configure(const std::vector& collections, const std::vector& packetExtractors, bool reverseOrder = false) { + ASSERT(collections.size() == packetExtractors.size()); + this->collections = collections; + this->packetExtractors = packetExtractors; + this->reverseOrder = reverseOrder; + numProcessedPackets = 0; + processedTotalLength = b(0); + } + int getNumProcessedPackets() const { return numProcessedPackets; } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestWrrScheduler : public queueing::WrrScheduler +{ + public: + void configure(const std::vector& collections, const std::vector& weights, const std::vector& buckets) { + ASSERT(collections.size() == weights.size()); + ASSERT(collections.size() == buckets.size()); + this->collections = collections; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + this->weights = new unsigned int[weights.size()]; + this->buckets = new unsigned int[buckets.size()]; + for (size_t i = 0; i < collections.size(); i++) { + this->weights[i] = weights[i]; + this->buckets[i] = buckets[i]; + } + numProcessedPackets = 0; + processedTotalLength = b(0); + } + unsigned int getBucket(int index) const { return buckets[index]; } + int getNumProcessedPackets() const { return numProcessedPackets; } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestLabelScheduler : public queueing::LabelScheduler +{ + public: + void configure(const std::vector& collections, const std::vector& labels, int defaultGateIndex) { + this->collections = collections; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + this->labels = labels; + this->defaultGateIndex = defaultGateIndex; + } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestPacketDepartureListener : public cListener +{ + public: + int numDequeuedPackets = 0; + int numRemovedPackets = 0; + int numDroppedPackets = 0; + int64_t lastDroppedPacketId = -1; + std::vector> expectedPacketCountsAtFirstDrop; + bool allExpectedPacketsDetachedAtFirstDrop = true; + + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (!source->isSubscribed(signal, this)) + return; + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DEQUEUED) + numDequeuedPackets++; + else if (reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) + numRemovedPackets++; + else if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED) { + if (numDroppedPackets == 0) + for (auto& [queue, expectedPacketCount] : expectedPacketCountsAtFirstDrop) + allExpectedPacketsDetachedAtFirstDrop &= queue->getNumPackets() == expectedPacketCount; + numDroppedPackets++; + lastDroppedPacketId = packet->getId(); + } + } +}; + +class TestReentrantRemovalListener : public cListener +{ + public: + queueing::IPacketQueue *queue = nullptr; + Packet *triggerPacket = nullptr; + Packet *packetToRemove = nullptr; + queueing::IPacketQueue::PacketRemovalReason triggerReason = queueing::IPacketQueue::PacketRemovalReason::REMOVED; + bool removedPacket = false; + + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (!source->isSubscribed(signal, this)) + return; + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + if (!removedPacket && packet == triggerPacket && reason == triggerReason) { + removedPacket = true; + queue->removePacket(packetToRemove); + } + } +}; + +class TestPacketPulledListener : public cListener +{ + public: + cComponent *leaf = nullptr; + cComponent *compound = nullptr; + int numLeafPulls = 0; + int numCompoundPulls = 0; + + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *, cObject *) override { + if (source == leaf) + numLeafPulls++; + else if (source == compound) + numCompoundPulls++; + } +}; + +class TestPacketArrivalListener : public cListener +{ + public: + int numSignals = 0; + cGate *lastArrivalGate = nullptr; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + numSignals++; + lastArrivalGate = check_and_cast(value)->getArrivalGate(); + } +}; +class TestOnlyPacketExtractor : public queueing::IPacketExtractor +{ + public: + std::vector packets; + + virtual Packet *findPacket(const PacketPredicate& predicate) const override { + auto it = std::find_if(packets.begin(), packets.end(), predicate); + return it == packets.end() ? nullptr : *it; + } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + auto packet = findPacket(predicate); + if (packet != nullptr) + packets.erase(std::find(packets.begin(), packets.end(), packet)); + return packet; + } +}; +static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiverAddress); + header->setTid(tid); + header->setSequenceNumber(sequenceNumber); + return header; +} +%activity: +MacAddress peer1("00:00:00:00:00:01"); +auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const char *dropperClass = "") { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("packetCapacity").setIntValue(packetCapacity); + module->par("dropperClass").setStringValue(dropperClass); + module->callInitialize(); + return check_and_cast(module); +}; +// Predicate extraction follows each provider's scheduling policy instead of +// the compound collection's gate-order enumeration. +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstPacket = new Packet("priorityFirstPacket", makeShared(B(1))); + auto secondPacket = new Packet("prioritySecondPacket", makeShared(B(1))); + firstQueue.packets = { firstPacket }; + secondQueue.packets = { secondPacket }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + auto predicate = [](const Packet *) { return true; }; + ASSERT(scheduler.findPacket(predicate) == secondPacket); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == secondPacket); + scheduler.release(secondPacket); + delete secondPacket; + firstQueue.removePacket(firstPacket); + delete firstPacket; +} + +// Priority extraction is independent of collection enumeration, so an input +// may support predicate extraction even when it cannot report aggregate size. +{ + TestOnlyPacketExtractor packetExtractor; + auto packet = new Packet("extractorOnlyPacket", makeShared(B(1))); + packetExtractor.packets = { packet }; + TestPriorityScheduler scheduler; + scheduler.configure(std::vector({ nullptr }), std::vector({ &packetExtractor })); + bool aggregateQueryFailed = false; + try { + scheduler.getNumPackets(); + } + catch (cRuntimeError& error) { + aggregateQueryFailed = std::string(error.what()).find("getNumPackets") != std::string::npos && std::string(error.what()).find("IPacketCollection") != std::string::npos; + } + ASSERT(aggregateQueryFailed); + bool aggregateLengthQueryFailed = false; + try { + scheduler.getTotalLength(); + } + catch (cRuntimeError& error) { + aggregateLengthQueryFailed = std::string(error.what()).find("getTotalLength") != std::string::npos && std::string(error.what()).find("IPacketCollection") != std::string::npos; + } + ASSERT(aggregateLengthQueryFailed); + ASSERT(scheduler.findPacket([](const Packet *) { return true; }) == packet); + auto dequeuedPacket = scheduler.dequeuePacket([](const Packet *) { return true; }); + ASSERT(dequeuedPacket == packet); + ASSERT(packetExtractor.packets.empty()); + scheduler.release(packet); + delete packet; +} +// Collection-only providers remain usable for aggregate queries and fail only +// when predicate extraction is actually requested. +{ + TestPacketQueue firstCollection; + TestPacketQueue collectionOnly; + auto firstPacket = new Packet("extractablePacket", makeShared(B(1))); + auto collectionOnlyPacket = new Packet("collectionOnlyPacket", makeShared(B(1))); + firstCollection.packets = { firstPacket }; + collectionOnly.packets = { collectionOnlyPacket }; + TestPriorityScheduler scheduler; + scheduler.configure(std::vector({ &firstCollection, &collectionOnly }), std::vector({ &firstCollection, nullptr })); + ASSERT(scheduler.getNumPackets() == 2); + bool extractionFailed = false; + try { + scheduler.findPacket([](const Packet *) { return true; }); + } + catch (cRuntimeError& error) { + extractionFailed = std::string(error.what()).find("findPacket") != std::string::npos && std::string(error.what()).find("IPacketExtractor") != std::string::npos; + } + ASSERT(extractionFailed); + firstCollection.removePacket(firstPacket); + collectionOnly.removePacket(collectionOnlyPacket); + delete firstPacket; + delete collectionOnlyPacket; +} +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstPacket = new Packet("wrrFirstPacket", makeShared(B(1))); + auto secondPacket = new Packet("wrrSecondPacket", makeShared(B(1))); + firstQueue.packets = { firstPacket }; + secondQueue.packets = { secondPacket }; + TestWrrScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { 1, 2 }, { 0, 2 }); + auto predicate = [](const Packet *) { return true; }; + ASSERT(scheduler.findPacket(predicate) == secondPacket); + ASSERT(scheduler.findPacket(predicate) == secondPacket); + ASSERT(scheduler.getBucket(1) == 2); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == secondPacket); + ASSERT(scheduler.getBucket(1) == 1); + scheduler.release(secondPacket); + delete secondPacket; + firstQueue.removePacket(firstPacket); + delete firstPacket; +} +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto blockedPacket = new Packet("labelBlockedPacket", makeShared(B(1))); + auto lowPacket = new Packet("labelLowPacket", makeShared(B(1))); + auto highPacket = new Packet("labelHighPacket", makeShared(B(1))); + auto lowLabels = lowPacket->addTag(); + lowLabels->setLabelsArraySize(1); + lowLabels->setLabels(0, "low"); + auto highLabels = highPacket->addTag(); + highLabels->setLabelsArraySize(1); + highLabels->setLabels(0, "high"); + firstQueue.packets = { blockedPacket, lowPacket }; + secondQueue.packets = { highPacket }; + TestLabelScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { "high", "low" }, 0); + auto predicate = [blockedPacket](const Packet *packet) { return packet != blockedPacket; }; + ASSERT(scheduler.findPacket(predicate) == highPacket); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == highPacket); + scheduler.release(highPacket); + delete highPacket; + firstQueue.removePacket(blockedPacket); + firstQueue.removePacket(lowPacket); + delete blockedPacket; + delete lowPacket; +} + + +// Closed gates expose no predicate-selected packet and remove nothing. Once +// opened, the same exact candidate crosses through PacketFlowBase accounting. +{ + auto leafQueue = createPacketQueue("closedGateLeaf"); + auto gateModule = cModuleType::get("inet.queueing.gate.PacketGate")->create("closedPredicateGate", this); + gateModule->par("initiallyOpen").setBoolValue(false); + gateModule->par("openTime").setDoubleValue(10); + gateModule->par("closeTime").setDoubleValue(20); + leafQueue->gate("out")->connectTo(gateModule->gate("in")); + gateModule->callInitialize(); + auto packet = new Packet("closedGatePacket", makeShared(B(1))); + leafQueue->pushPacket(packet, nullptr); + auto packetExtractor = check_and_cast(gateModule); + auto closedFoundPacket = packetExtractor->findPacket([](const Packet *) { return true; }); + auto closedDequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(closedFoundPacket == nullptr); + ASSERT(closedDequeuedPacket == nullptr); + ASSERT(leafQueue->getNumPackets() == 1); + check_and_cast(gateModule)->open(); + ASSERT(packetExtractor->findPacket([](const Packet *) { return true; }) == packet); + auto dequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(dequeuedPacket == packet); + ASSERT(leafQueue->getNumPackets() == 0); + take(packet); + delete packet; + gateModule->callFinish(); + leafQueue->callFinish(); + gateModule->deleteModule(); + leafQueue->deleteModule(); +} + +// An implicit guard band rejects the provider's exact first match instead of +// skipping it and extracting a later packet that happens to fit. +{ + auto leafQueue = createPacketQueue("guardBandLeaf"); + auto gateModule = cModuleType::get("inet.queueing.gate.PeriodicGate")->create("predicateGuardBandGate", this); + auto durations = new cValueArray(); + durations->add(cValue(1, "s")); + durations->add(cValue(1, "s")); + gateModule->par("durations").setObjectValue(durations); + gateModule->par("initiallyOpen").setBoolValue(true); + gateModule->par("bitrate").setDoubleValue(8); + leafQueue->gate("out")->connectTo(gateModule->gate("in")); + gateModule->callInitialize(); + auto blockedPacket = new Packet("guardBandBlockedPacket", makeShared(B(2))); + auto fittingPacket = new Packet("guardBandFittingPacket", makeShared(B(1))); + leafQueue->pushPacket(blockedPacket, nullptr); + leafQueue->pushPacket(fittingPacket, nullptr); + auto packetExtractor = check_and_cast(gateModule); + auto blockedFoundPacket = packetExtractor->findPacket([](const Packet *) { return true; }); + auto blockedDequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(blockedFoundPacket == nullptr); + ASSERT(blockedDequeuedPacket == nullptr); + ASSERT(leafQueue->getNumPackets() == 2); + auto dequeuedPacket = packetExtractor->dequeuePacket([blockedPacket](const Packet *packet) { return packet != blockedPacket; }); + ASSERT(dequeuedPacket == fittingPacket); + ASSERT(leafQueue->getNumPackets() == 1); + take(fittingPacket); + delete fittingPacket; + auto retainedPacket = leafQueue->dequeuePacket([blockedPacket](const Packet *packet) { return packet == blockedPacket; }); + ASSERT(retainedPacket == blockedPacket); + take(blockedPacket); + delete blockedPacket; + gateModule->callFinish(); + leafQueue->callFinish(); + gateModule->deleteModule(); + leafQueue->deleteModule(); +} +// A real queue with a downstream collector predicate-dequeues a non-front +// packet, records its residence and semantic pull once, and animates it to the +// collector while retaining the front packet. +{ + TestPacketPulledListener pullListener; + TestPacketDepartureListener removalCallback; + auto leafQueueModule = cModuleType::get("inet.queueing.queue.PacketQueue")->create("selectedDequeueQueue", this); + auto sinkModule = cModuleType::get("inet.queueing.sink.FullPacketSink")->create("selectedDequeueSink", this); + leafQueueModule->gate("out")->connectTo(sinkModule->gate("in")); + leafQueueModule->callInitialize(); + sinkModule->callInitialize(); + auto leafQueue = check_and_cast(leafQueueModule); + pullListener.leaf = leafQueue; + leafQueue->subscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + auto frontPacket = new Packet("selectedDequeueFront", makeShared(B(1))); + auto selectedPacket = new Packet("selectedDequeueMatch", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))); + auto queueingTimeTag = selectedPacket->addRegionTag(); + queueingTimeTag->setBitTotalTimesArraySize(1); + queueingTimeTag->setBitTotalTimes(0, 0); + queueingTimeTag->setPacketTotalTimesArraySize(1); + queueingTimeTag->setPacketTotalTimes(0, 0); + selectedPacket->addRegionTag(); + selectedPacket->setArrivalTime(simTime() - 1); + leafQueue->pushPacket(frontPacket, nullptr); + leafQueue->pushPacket(selectedPacket, nullptr); + auto dequeuedPacket = leafQueue->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + ASSERT(dequeuedPacket == selectedPacket); + int numQueuedEvents = 0; + selectedPacket->mapAllRegionTags(b(0), selectedPacket->getTotalLength(), [&](b, b, const Ptr& tag) { + for (size_t i = 0; i < tag->getPacketEventsArraySize(); i++) + if (tag->getPacketEvents(i)->getKind() == PEK_QUEUED) + numQueuedEvents++; + }); + simtime_t totalQueueingTime = -1; + selectedPacket->mapAllRegionTags(b(0), selectedPacket->getTotalLength(), [&](b, b, const Ptr& tag) { + ASSERT(tag->getPacketTotalTimesArraySize() == 1); + totalQueueingTime = tag->getPacketTotalTimes(0); + }); + ASSERT(numQueuedEvents == 1); + ASSERT(totalQueueingTime == 1); + ASSERT(leafQueue->getNumPackets() == 1); + ASSERT(leafQueue->getPacket(0) == frontPacket); + ASSERT(pullListener.numLeafPulls == 1); + ASSERT(removalCallback.numDequeuedPackets == 1); + ASSERT(selectedPacket->getArrivalGate() == sinkModule->gate("in")); + leafQueue->unsubscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + leafQueue->removePacket(frontPacket); + take(frontPacket); + take(selectedPacket); + delete frontPacket; + delete selectedPacket; + sinkModule->callFinish(); + leafQueueModule->callFinish(); + sinkModule->deleteModule(); + leafQueueModule->deleteModule(); +} + +// Predicate extraction through a concrete PacketFlowBase module processes a +// non-front packet once. The queue animates it to the flow input before the +// flow signals, and the flow then animates it to the final collector. +{ + TestPacketPulledListener pullListener; + TestPacketDepartureListener removalCallback; + TestPacketArrivalListener flowInputListener; + TestPacketArrivalListener flowOutputListener; + auto leafQueueModule = cModuleType::get("inet.queueing.queue.PacketQueue")->create("selectedFlowLeaf", this); + auto gateModule = cModuleType::get("inet.queueing.gate.PacketGate")->create("selectedFlowGate", this); + auto sinkModule = cModuleType::get("inet.queueing.sink.FullPacketSink")->create("selectedFlowSink", this); + gateModule->par("initiallyOpen").setBoolValue(true); + gateModule->par("openTime").setDoubleValue(10); + gateModule->par("closeTime").setDoubleValue(20); + leafQueueModule->gate("out")->connectTo(gateModule->gate("in")); + gateModule->gate("out")->connectTo(sinkModule->gate("in")); + leafQueueModule->callInitialize(); + gateModule->callInitialize(); + sinkModule->callInitialize(); + auto leafQueue = check_and_cast(leafQueueModule); + auto packetGate = check_and_cast(gateModule); + auto packetExtractor = check_and_cast(gateModule); + pullListener.leaf = leafQueue; + leafQueue->subscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + gateModule->subscribe(packetPulledInSignal, &flowInputListener); + gateModule->subscribe(packetPulledOutSignal, &flowOutputListener); + auto frontPacket = new Packet("selectedFlowFront", makeShared(B(1))); + auto selectedPacket = new Packet("selectedFlowMatch", makeShared(B(1))); + leafQueue->pushPacket(frontPacket, nullptr); + leafQueue->pushPacket(selectedPacket, nullptr); + auto dequeuedPacket = packetExtractor->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + ASSERT(dequeuedPacket == selectedPacket); + ASSERT(leafQueue->getNumPackets() == 1); + ASSERT(leafQueue->getPacket(0) == frontPacket); + ASSERT(pullListener.numLeafPulls == 1); + ASSERT(removalCallback.numDequeuedPackets == 1); + ASSERT(flowInputListener.numSignals == 1); + ASSERT(flowInputListener.lastArrivalGate == gateModule->gate("in")); + ASSERT(flowOutputListener.numSignals == 1); + ASSERT(flowOutputListener.lastArrivalGate == gateModule->gate("in")); + ASSERT(packetGate->resolveDirective('p') == "1"); + ASSERT(selectedPacket->getArrivalGate() == sinkModule->gate("in")); + leafQueue->unsubscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + gateModule->unsubscribe(packetPulledInSignal, &flowInputListener); + gateModule->unsubscribe(packetPulledOutSignal, &flowOutputListener); + leafQueue->removePacket(frontPacket); + take(frontPacket); + take(selectedPacket); + delete frontPacket; + delete selectedPacket; + sinkModule->callFinish(); + gateModule->callFinish(); + leafQueueModule->callFinish(); + sinkModule->deleteModule(); + gateModule->deleteModule(); + leafQueueModule->deleteModule(); +} +// The typed queue callback covers every non-destructive ownership departure, +// including bulk removal, exactly once at the queue boundary. +{ + auto lifecycleQueue = createPacketQueue("lifecycleQueue"); + TestPacketDepartureListener lifecycleCallback; + check_and_cast(lifecycleQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &lifecycleCallback); + auto pulledPacket = new Packet("lifecyclePulled", makeShared(B(1))); + auto selectedPacket = new Packet("lifecycleSelected", makeShared(B(1))); + auto removedPacket = new Packet("lifecycleRemoved", makeShared(B(1))); + auto firstBulkRemovedPacket = new Packet("lifecycleBulkRemovedFirst", makeShared(B(1))); + auto secondBulkRemovedPacket = new Packet("lifecycleBulkRemovedSecond", makeShared(B(1))); + auto thirdBulkRemovedPacket = new Packet("lifecycleBulkRemovedThird", makeShared(B(1))); + lifecycleQueue->pushPacket(pulledPacket, nullptr); + lifecycleQueue->pushPacket(selectedPacket, nullptr); + lifecycleQueue->pushPacket(removedPacket, nullptr); + lifecycleQueue->pushPacket(firstBulkRemovedPacket, nullptr); + lifecycleQueue->pushPacket(secondBulkRemovedPacket, nullptr); + lifecycleQueue->pushPacket(thirdBulkRemovedPacket, nullptr); + auto pulledResult = lifecycleQueue->dequeuePacket(); + auto selectedResult = lifecycleQueue->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + lifecycleQueue->removePacket(removedPacket); + take(pulledResult); + take(selectedResult); + take(removedPacket); + delete pulledResult; + delete selectedResult; + delete removedPacket; + lifecycleQueue->removeAllPackets(); + if (lifecycleCallback.numDequeuedPackets != 2 || lifecycleCallback.numRemovedPackets != 4 || lifecycleCallback.numDroppedPackets != 0 || !lifecycleQueue->isEmpty()) + throw cRuntimeError("Typed queue lifecycle callback did not cover every departure exactly once"); + check_and_cast(lifecycleQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &lifecycleCallback); + lifecycleQueue->callFinish(); + lifecycleQueue->deleteModule(); +} + + +// Bulk removal detaches only this queue's packets from a shared buffer. A +// direct buffer removal then notifies the owning queue exactly once. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("sharedRemovalBuffer", this); + bufferModule->callInitialize(); + auto createBufferedQueue = [&](const char *name) { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("bufferModule").setStringValue("^.sharedRemovalBuffer"); + module->callInitialize(); + return check_and_cast(module); + }; + auto firstQueue = createBufferedQueue("sharedRemovalFirstQueue"); + auto secondQueue = createBufferedQueue("sharedRemovalSecondQueue"); + TestPacketDepartureListener firstCallback; + TestPacketDepartureListener secondCallback; + TestPacketDepartureListener compoundCallback; + TestCompoundPacketQueue bufferedCompound; + bufferedCompound.observeQueue(secondQueue); + bufferedCompound.addObserver(&compoundCallback); + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &firstCallback); + check_and_cast(secondQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &secondCallback); + firstQueue->pushPacket(new Packet("sharedRemovalFirstA", makeShared(B(1))), nullptr); + firstQueue->pushPacket(new Packet("sharedRemovalFirstB", makeShared(B(1))), nullptr); + auto retainedPacket = new Packet("sharedRemovalRetained", makeShared(B(1))); + secondQueue->pushPacket(retainedPacket, nullptr); + firstQueue->removeAllPackets(); + auto buffer = check_and_cast(bufferModule); + ASSERT(firstQueue->isEmpty()); + ASSERT(secondQueue->getNumPackets() == 1); + ASSERT(buffer->getNumPackets() == 1); + ASSERT(firstCallback.numRemovedPackets == 2); + ASSERT(secondCallback.numRemovedPackets == 0); + buffer->removePacket(retainedPacket); + ASSERT(secondQueue->isEmpty()); + ASSERT(buffer->getNumPackets() == 0); + ASSERT(secondCallback.numRemovedPackets == 1); + ASSERT(secondCallback.numDroppedPackets == 0); + ASSERT(compoundCallback.numRemovedPackets == 1); + ASSERT(compoundCallback.numDroppedPackets == 0); + take(retainedPacket); + delete retainedPacket; + bufferedCompound.stopObserving(); + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &firstCallback); + check_and_cast(secondQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &secondCallback); + firstQueue->callFinish(); + secondQueue->callFinish(); + bufferModule->callFinish(); + firstQueue->deleteModule(); + secondQueue->deleteModule(); + bufferModule->deleteModule(); +} + +// A shared buffer rejects cPacketQueue ownership it cannot detach before any +// mutation, preventing a later overload drop from corrupting the owner queue. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("unsupportedOwnerBuffer", this); + bufferModule->callInitialize(); + auto buffer = check_and_cast(bufferModule); + cPacketQueue unsupportedOwnerQueue("unsupportedOwnerQueue"); + auto packet = new Packet("unsupportedOwnerPacket", makeShared(B(1))); + unsupportedOwnerQueue.insert(packet); + bool unsupportedOwnerRejected = false; + try { + buffer->addPacket(packet); + } + catch (cRuntimeError& error) { + unsupportedOwnerRejected = std::string(error.what()).find("IPacketBuffer::ICallback") != std::string::npos; + } + ASSERT(unsupportedOwnerRejected); + ASSERT(buffer->getNumPackets() == 0); + ASSERT(unsupportedOwnerQueue.contains(packet)); + unsupportedOwnerQueue.remove(packet); + take(packet); + delete packet; + bufferModule->callFinish(); + bufferModule->deleteModule(); +} + +// Internal overflow invokes the typed queue callback once for the destructive +// discard and never for the packet later removed normally. +{ + auto overflowQueue = createPacketQueue("overflowQueue", 1, "inet::queueing::PacketAtCollectionEndDropper"); + TestPacketDepartureListener dropCallback; + check_and_cast(overflowQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto retainedPacket = new Packet("retainedPacket", makeShared(B(1))); + auto overflowPacket = new Packet("overflowPacket", makeShared(B(1))); + auto overflowPacketId = overflowPacket->getId(); + overflowQueue->pushPacket(retainedPacket, nullptr); + overflowQueue->pushPacket(overflowPacket, nullptr); + ASSERT(dropCallback.numDroppedPackets == 1); + ASSERT(dropCallback.lastDroppedPacketId == overflowPacketId); + check_and_cast(overflowQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto dequeuedPacket = overflowQueue->dequeuePacket([retainedPacket](const Packet *packet) { return packet == retainedPacket; }); + ASSERT(dequeuedPacket == retainedPacket); + ASSERT(dropCallback.numDroppedPackets == 1); + overflowQueue->callFinish(); + overflowQueue->deleteModule(); +} + +// A compound unregisters from child queues during pre-delete even when finish +// is skipped, and repeated lifecycle cleanup is harmless. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("preDeletePendingQueue", this); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + auto leafQueue = check_and_cast(pendingQueueModule->getSubmodule("managementQueue")); + TestPacketDepartureListener callback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &callback); + + auto observedPacket = new Packet("preDeleteObservedPacket", makeShared(B(1))); + leafQueue->pushPacket(observedPacket, nullptr); + leafQueue->removePacket(observedPacket); + ASSERT(callback.numRemovedPackets == 1); + take(observedPacket); + delete observedPacket; + + pendingQueueModule->callPreDelete(pendingQueueModule); + auto unobservedPacket = new Packet("preDeleteUnobservedPacket", makeShared(B(1))); + leafQueue->pushPacket(unobservedPacket, nullptr); + leafQueue->removePacket(unobservedPacket); + ASSERT(callback.numRemovedPackets == 1); + take(unobservedPacket); + delete unobservedPacket; + + pendingQueueModule->deleteModule(); +} + +// A compound's direct frontier and a nested compound boundary each forward +// descendant removals exactly once. Boundary-initiated removals suppress the +// descendant signal for the same packet, including during reentrant removal +// of a different packet. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("callbackPendingQueue", this); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + auto directLeaf = check_and_cast(pendingQueueModule->getSubmodule("managementQueue")); + TestPacketDepartureListener directCallback; + TestPacketDepartureListener nestedCallback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + TestCompoundPacketQueue outerCompound; + outerCompound.observeQueue(pendingQueue); + outerCompound.addObserver(&nestedCallback); + + auto directPacket = new Packet("directCompoundDrop", makeShared(B(1))); + directLeaf->handlePacketDropped(directPacket); + ASSERT(directCallback.numDroppedPackets == 1); + ASSERT(nestedCallback.numDroppedPackets == 1); + delete directPacket; + + auto leafRemovedPacket = new Packet("leafRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(leafRemovedPacket, nullptr); + directLeaf->removePacket(leafRemovedPacket); + ASSERT(directCallback.numRemovedPackets == 1); + ASSERT(nestedCallback.numRemovedPackets == 1); + take(leafRemovedPacket); + delete leafRemovedPacket; + + directLeaf->pushPacket(new Packet("leafBulkRemovedFirst", makeShared(B(1))), nullptr); + directLeaf->pushPacket(new Packet("leafBulkRemovedSecond", makeShared(B(1))), nullptr); + directLeaf->removeAllPackets(); + ASSERT(directCallback.numRemovedPackets == 3); + ASSERT(nestedCallback.numRemovedPackets == 3); + + auto boundaryRemovedPacket = new Packet("boundaryRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryRemovedPacket, nullptr); + pendingQueue->removePacket(boundaryRemovedPacket); + ASSERT(directCallback.numRemovedPackets == 4); + ASSERT(nestedCallback.numRemovedPackets == 4); + take(boundaryRemovedPacket); + delete boundaryRemovedPacket; + + directLeaf->pushPacket(new Packet("boundaryBulkRemovedFirst", makeShared(B(1))), nullptr); + directLeaf->pushPacket(new Packet("boundaryBulkRemovedSecond", makeShared(B(1))), nullptr); + pendingQueue->removeAllPackets(); + ASSERT(directCallback.numRemovedPackets == 6); + ASSERT(nestedCallback.numRemovedPackets == 6); + + auto boundaryDequeuedPacket = new Packet("boundaryDequeuedPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryDequeuedPacket, nullptr); + auto dequeuedPacket = pendingQueue->dequeuePacket([boundaryDequeuedPacket](const Packet *packet) { return packet == boundaryDequeuedPacket; }); + ASSERT(dequeuedPacket == boundaryDequeuedPacket); + ASSERT(directCallback.numDequeuedPackets == 1); + ASSERT(nestedCallback.numDequeuedPackets == 0); + take(dequeuedPacket); + delete dequeuedPacket; + + auto boundaryPulledPacket = new Packet("boundaryPulledPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryPulledPacket, nullptr); + auto pulledPacket = pendingQueue->pullPacket(nullptr); + ASSERT(pulledPacket == boundaryPulledPacket); + ASSERT(directCallback.numDequeuedPackets == 2); + ASSERT(nestedCallback.numDequeuedPackets == 0); + take(pulledPacket); + delete pulledPacket; + + auto reentrantTriggerPacket = new Packet("reentrantTriggerPacket", makeShared(B(1))); + auto reentrantRemovedPacket = new Packet("reentrantRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(reentrantTriggerPacket, nullptr); + directLeaf->pushPacket(reentrantRemovedPacket, nullptr); + TestReentrantRemovalListener reentrantCallback; + reentrantCallback.queue = directLeaf; + reentrantCallback.triggerPacket = reentrantTriggerPacket; + reentrantCallback.packetToRemove = reentrantRemovedPacket; + check_and_cast(directLeaf)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + pendingQueue->removePacket(reentrantTriggerPacket); + ASSERT(reentrantCallback.removedPacket); + ASSERT(directCallback.numRemovedPackets == 8); + ASSERT(nestedCallback.numRemovedPackets == 8); + check_and_cast(directLeaf)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + take(reentrantTriggerPacket); + take(reentrantRemovedPacket); + delete reentrantTriggerPacket; + delete reentrantRemovedPacket; + + outerCompound.stopObserving(); + check_and_cast(pendingQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + pendingQueueModule->callFinish(); + pendingQueueModule->deleteModule(); + + // Starting the frontier at the enclosing module forces traversal through + // PacketShaper, which is not an IPacketQueue, before reaching its leaf. + auto packetShaperModule = cModuleType::get("inet.queueing.shaper.PacketShaper")->create("callbackPacketShaper", this); + packetShaperModule->callInitialize(); + auto wrappedLeaf = check_and_cast(packetShaperModule->getSubmodule("queue")); + TestPacketDepartureListener wrappedCallback; + TestCompoundPacketQueue wrappedCompound; + wrappedCompound.observeDescendantQueues(this); + wrappedCompound.addObserver(&wrappedCallback); + auto wrappedPacket = new Packet("wrappedCompoundDrop", makeShared(B(1))); + wrappedLeaf->handlePacketDropped(wrappedPacket); + ASSERT(wrappedCallback.numDroppedPackets == 1); + delete wrappedPacket; + wrappedCompound.stopObserving(); + packetShaperModule->callFinish(); + packetShaperModule->deleteModule(); +} + +// Compound capacity enforcement detaches its leaf victim without leaking a +// REMOVED callback before reporting the single destructive departure. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("callbackOverflowPendingQueue", this); + pendingQueueModule->par("packetCapacity").setIntValue(1); + pendingQueueModule->par("dropperClass").setStringValue("inet::queueing::PacketAtCollectionEndDropper"); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + TestPacketDepartureListener directCallback; + TestPacketDepartureListener nestedCallback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + TestCompoundPacketQueue outerCompound; + outerCompound.observeQueue(pendingQueue); + outerCompound.addObserver(&nestedCallback); + pendingQueue->pushPacket(new Packet("compoundRetainedPacket", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))), nullptr); + pendingQueue->pushPacket(new Packet("compoundOverflowPacket", makeQosHeader(peer1, 1, SequenceNumberCyclic(2))), nullptr); + ASSERT(pendingQueue->getNumPackets() == 1); + ASSERT(directCallback.numDroppedPackets == 1); + ASSERT(directCallback.numRemovedPackets == 0); + ASSERT(nestedCallback.numDroppedPackets == 1); + ASSERT(nestedCallback.numRemovedPackets == 0); + outerCompound.stopObserving(); + check_and_cast(pendingQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + pendingQueue->removeAllPackets(); + pendingQueueModule->callFinish(); + pendingQueueModule->deleteModule(); +} + +// A shared buffer detaches every victim from its owning queue before the first +// typed drop callback. This keeps protocol cleanup from reentering while a +// later victim is still selectable from another queue. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("sharedOverflowBuffer", this); + bufferModule->par("dataCapacity").setIntValue(24); + bufferModule->par("dropperClass").setStringValue("inet::queueing::PacketAtCollectionBeginDropper"); + bufferModule->callInitialize(); + auto createBufferedQueue = [&](const char *name) { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("bufferModule").setStringValue("^.sharedOverflowBuffer"); + module->callInitialize(); + return check_and_cast(module); + }; + auto firstQueue = createBufferedQueue("sharedOverflowFirstQueue"); + auto secondQueue = createBufferedQueue("sharedOverflowSecondQueue"); + TestPacketDepartureListener dropCallback; + dropCallback.expectedPacketCountsAtFirstDrop = { { firstQueue, 1 }, { secondQueue, 0 } }; + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + check_and_cast(secondQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto firstVictim = new Packet("sharedFirstVictim", makeShared(B(1))); + auto retainedPacket = new Packet("sharedRetainedPacket", makeShared(B(3))); + TestReentrantRemovalListener reentrantCallback; + reentrantCallback.queue = firstQueue; + reentrantCallback.triggerPacket = firstVictim; + reentrantCallback.packetToRemove = retainedPacket; + reentrantCallback.triggerReason = queueing::IPacketQueue::PacketRemovalReason::DROPPED; + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + firstQueue->pushPacket(firstVictim, nullptr); + secondQueue->pushPacket(new Packet("sharedSecondVictim", makeShared(B(1))), nullptr); + firstQueue->pushPacket(retainedPacket, nullptr); + if (dropCallback.numDroppedPackets != 2 || dropCallback.numRemovedPackets != 1 || !dropCallback.allExpectedPacketsDetachedAtFirstDrop || !reentrantCallback.removedPacket || firstQueue->getNumPackets() != 0 || secondQueue->getNumPackets() != 0) + throw cRuntimeError("Shared buffer reentrant removal violated the detached victim batch contract"); + take(retainedPacket); + delete retainedPacket; + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + check_and_cast(secondQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + firstQueue->callFinish(); + secondQueue->callFinish(); + bufferModule->callFinish(); + firstQueue->deleteModule(); + secondQueue->deleteModule(); + bufferModule->deleteModule(); +} +EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; +%contains: stdout +ADDBA transaction and negotiated SSN integrity checks passed. From 2d11de0dce7ad56ba1302d5ede218326555815df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:38:12 +0200 Subject: [PATCH 29/91] ieee80211: add+fix: make originator ADDBA transactions explicit An ADDBA request can be rejected before transmission, retransmitted or answered late. Track its identity, transmission state and response deadline explicitly, match responses to the live transaction, and restore eligible data after terminal failure. A real HCF/RED regression covers rejection before enqueue. Recipient responses echo the request token, completing the wire contract required by originator validation. Block Ack baselines reflect transmitted-request deadlines, live token matching and peer/TID eligibility. Refresh the three blockack showcases, HCFfragblockack, txop/General and MacQosWithBlockAck on master 7287f347aa, which includes the corrected 20-byte BAR from 9a9e65cbb5. The preceding checkpoint preserves master values. MacQosWithBlockAck records its intermediate trajectory here; the later A-MSDU policy fix owns its next transition. Non-Block-Ack graphical changes retain the accepted queue-counter updates: aggregate service dequeues members instead of removing them. The user approved these exact refreshed expectations after the 2026-09-15 attribution report. Scoped debug verification uses run 0 and all declared ingredients. Transaction and real RED/HCF tests cover terminal outcomes. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211 | behavior.add+change.fix | fingerprint test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 13 + doc/src/migration-guide/index.rst | 28 + .../aggregation/BasicMsduAggregationPolicy.cc | 60 +- .../aggregation/BasicMsduAggregationPolicy.h | 2 +- .../blockack/Ieee80211AddbaTransactionTag.msg | 16 + .../blockack/OriginatorBlockAckAgreement.h | 26 +- .../OriginatorBlockAckAgreementHandler.cc | 286 ++- .../OriginatorBlockAckAgreementHandler.h | 29 +- .../OriginatorBlockAckAgreementPolicy.cc | 16 +- .../OriginatorBlockAckAgreementPolicy.h | 6 +- .../OriginatorBlockAckAgreementPolicy.ned | 2 + .../RecipientBlockAckAgreementHandler.cc | 2 + .../ieee80211/mac/channelaccess/Edca.h | 1 + .../IBlockAckAgreementHandlerCallback.h | 5 + .../mac/contract/IMsduAggregationPolicy.h | 6 +- .../IOriginatorBlockAckAgreementHandler.h | 34 +- .../IOriginatorBlockAckAgreementPolicy.h | 3 +- .../mac/contract/IOriginatorMacDataService.h | 8 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 6 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 397 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 25 +- .../ieee80211/mac/framesequence/HcfFs.cc | 22 +- .../mac/originator/NonQosRecoveryProcedure.cc | 33 +- .../mac/originator/NonQosRecoveryProcedure.h | 3 + .../originator/OriginatorMacDataService.cc | 21 +- .../mac/originator/OriginatorMacDataService.h | 4 + .../originator/OriginatorQosMacDataService.cc | 79 +- .../originator/OriginatorQosMacDataService.h | 6 +- .../ieee80211/mac/queue/InProgressFrames.cc | 24 +- .../ieee80211/mac/queue/InProgressFrames.h | 2 + .../ieee80211/mgmt/Ieee80211MgmtFrame.msg | 1 + tests/fingerprint/examples.csv | 10 +- tests/fingerprint/showcases.csv | 16 +- tests/module/Ieee80211HcfAddbaRedDrop_1.test | 176 ++ tests/unit/Ieee80211AddbaTransaction_1.test | 1626 +++++++++++++++++ 35 files changed, 2805 insertions(+), 189 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg create mode 100644 tests/module/Ieee80211HcfAddbaRedDrop_1.test diff --git a/WHATSNEW b/WHATSNEW index 3d4a90f80e1..a4bbc67ea29 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -328,6 +328,19 @@ Notable backward incompatible changes are the following: nested compound queues exactly once. PacketBuffer rejects packets owned by a cPacketQueue whose owner cannot participate in the buffer callback contract. +5. Block Ack DELBA agreement ownership + + Custom originator agreement handlers must explicitly implement + isDelbaPending(). + +6. Originator ADDBA Transactions and Eligible Frames + + ADDBA response deadlines now begin at actual request transmission. Frames for a + pending peer/TID wait for a matching successful response or terminal failure, then + channel access resumes for eligible traffic. This changes affected Block Ack + trajectories. A-MSDU service now appears as dequeued traffic in queue counters and + graphical fingerprints, instead of administrative removal. + 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 0092d3b45fc..e34aef498bc 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -75,6 +75,34 @@ deletes it. size, or provide collection-capable inputs. Predicate extraction similarly requires an extraction-capable provider only when that operation is used. +Originator ADDBA Transactions and Eligible Frames +------------------------------------------------- + +Custom ``IOriginatorBlockAckAgreementPolicy`` implementations must replace +``computeAddbaFailureTimeout()`` with ``getAddbaResponseTimeout()`` and implement +``computeAddbaRetryBackoff()``. Configure ``addbaResponseTimeout`` and +``addbaRetryBackoff`` on the originator agreement policy. The response interval begins +when the request is actually transmitted, so rejecting an unsent request must not arm +it. + +Custom originator data services must implement ``setFrameEligibilityFunction()``, +``isFrameEligible()`` and ``hasEligibleFrame()``. Keep availability queries free of +dequeue side effects. A-MSDU policies now receive the provider-selected anchor and +eligibility predicate; retain that anchor first and select only available, unique, +eligible members in valid flow order. Dequeue those members through the provider, +including scheduler accounting. Successful A-MSDU service therefore increments queue +dequeue counters instead of administrative-removal counters. + +Custom originator agreement handlers must explicitly implement ``isDelbaPending()``. +Adapt ADDBA response callers to the typed outcome and DELBA callers to ownership +transfer through ``unique_ptr`` where declared by the interface. Do not keep an +agreement pointer across a callback that may replace or remove it. + +Custom ADDBA request producers must assign a nonzero Dialog Token and preserve the local +transaction tag through retries and fragments. Recipient responses must echo the request +token. An originator accepts only a matching live response; a response using the old +default token zero no longer completes a transaction. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index 2d06484c6a4..e71051047bb 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc @@ -56,38 +56,60 @@ bool BasicMsduAggregationPolicy::isEligible(Packet *packet, const Ptr *BasicMsduAggregationPolicy::computeAggregateFrames(queueing::IPacketQueue *queue) +std::vector *BasicMsduAggregationPolicy::computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) { Enter_Method("computeAggregateFrames"); - ASSERT(!queue->isEmpty()); + ASSERT(candidate != nullptr); b aMsduLength = b(0); - Ptr firstHeader = nullptr; - auto frames = new std::vector(); + int candidateIndex = -1; for (int i = 0; i < queue->getNumPackets(); i++) { - auto dataPacket = queue->getPacket(i); + if (queue->getPacket(i) == candidate) { candidateIndex = i; break; } + } + if (candidateIndex == -1) + return nullptr; + const auto& firstHeader = dynamicPtrCast(candidate->peekAtFront()); + if (firstHeader == nullptr || !isFrameEligible(candidate)) + return nullptr; + auto frames = new std::vector(); + auto hasSameFlow = [&](const Ptr& dataHeader) { + return dataHeader != nullptr && dataHeader->getTid() == firstHeader->getTid() && + dataHeader->getReceiverAddress() == firstHeader->getReceiverAddress() && + dataHeader->getTransmitterAddress() == firstHeader->getTransmitterAddress(); + }; + auto appendIfEligible = [&](Packet *dataPacket) { const auto& dataHeader = dynamicPtrCast(dataPacket->peekAtFront()); - if (dataHeader == nullptr) - break; - if (firstHeader == nullptr) - firstHeader = dataHeader; + if (!hasSameFlow(dataHeader)) + return true; + // IEEE Std 802.11-2024, 5.1.3: preserve the ordering of MSDUs with the + // same traffic identifier. Enumeration order is the conservative + // intra-flow order for this built-in policy. Never overtake a + // same-flow packet which is held by transaction eligibility or cannot + // fit in the current A-MSDU. + if (!isFrameEligible(dataPacket)) + return false; const auto& dataTrailer = dataPacket->peekAtBack(B(4)); - if (!isEligible(dataPacket, staticPtrCast(dataHeader), dataTrailer, firstHeader, aMsduLength)) { - EV_TRACE << "Queued " << *dataPacket << " is not eligible for A-MSDU aggregation.\n"; - break; - } - EV_TRACE << "Queued " << *dataPacket << " is eligible for A-MSDU aggregation.\n"; + if (!isEligible(dataPacket, dataHeader, dataTrailer, firstHeader, aMsduLength)) + return false; frames->push_back(dataPacket); - aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); // sum of MSDU lengths + subframe header + aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); + return true; + }; + if (!appendIfEligible(candidate)) { + delete frames; + return nullptr; } + // Do not wrap around: providers may schedule in an order different from + // their IPacketCollection enumeration (for example reverse priority). + for (int i = candidateIndex + 1; i < queue->getNumPackets(); i++) + if (!appendIfEligible(queue->getPacket(i))) + break; if (frames->size() <= 1 || !isAggregationPossible(frames->size(), aMsduLength.get())) { EV_DEBUG << "A-MSDU aggregation is not possible, collected " << frames->size() << " packets.\n"; delete frames; return nullptr; } - else { - EV_DEBUG << "A-MSDU aggregation is possible, collected " << frames->size() << " packets.\n"; - return frames; - } + EV_DEBUG << "A-MSDU aggregation is possible, collected " << frames->size() << " packets.\n"; + return frames; } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h index 0534914eaf4..22ddab978ea 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h @@ -28,7 +28,7 @@ class INET_API BasicMsduAggregationPolicy : public IMsduAggregationPolicy, publi virtual bool isEligible(Packet *packet, const Ptr& header, const Ptr& trailer, const Ptr& testHeader, b aMsduLength); public: - virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue) override; + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) override; }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg new file mode 100644 index 00000000000..43d9033c743 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg @@ -0,0 +1,16 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; + +namespace inet::ieee80211; + +// Identifies one local originator ADDBA transaction across packet transformations. +class Ieee80211AddbaTransactionTag extends TagBase +{ + uint64_t transactionId; +} diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index a516527bc34..4533d0c18fa 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -15,6 +15,12 @@ namespace ieee80211 { class OriginatorBlockAckAgreementHandler; +enum class OriginatorBlockAckAgreementState +{ + PENDING, + ESTABLISHED, +}; + class INET_API OriginatorBlockAckAgreement : public cObject { protected: @@ -25,19 +31,24 @@ class INET_API OriginatorBlockAckAgreement : public cObject int bufferSize = -1; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; - bool isAddbaResponseReceived = false; + OriginatorBlockAckAgreementState state = OriginatorBlockAckAgreementState::PENDING; bool isAddbaRequestSent = false; + uint8_t dialogToken = 0; + uint64_t transactionId = 0; + simtime_t addbaResponseDeadline = -1; simtime_t blockAckTimeoutValue = -1; simtime_t expirationTime = -1; public: - OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported) : + OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported, uint8_t dialogToken, uint64_t transactionId) : receiverAddr(receiverAddr), tid(tid), startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), isAMsduSupported(isAMsduSupported), - isDelayedBlockAckPolicySupported(isDelayedBlockAckPolicySupported) + isDelayedBlockAckPolicySupported(isDelayedBlockAckPolicySupported), + dialogToken(dialogToken), + transactionId(transactionId) { } @@ -46,7 +57,8 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() { return startingSequenceNumber; } virtual void setStartingSequenceNumber(SequenceNumberCyclic sequenceNumber) { startingSequenceNumber = sequenceNumber; } - virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; } + virtual bool getIsAddbaResponseReceived() const { return state == OriginatorBlockAckAgreementState::ESTABLISHED; } + virtual bool isPending() const { return state == OriginatorBlockAckAgreementState::PENDING; } virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; } virtual bool getIsAMsduSupported() const { return isAMsduSupported; } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } @@ -54,13 +66,17 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual Tid getTid() const { return tid; } virtual const simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getNumSentBaPolicyFrames() const { return numSentBaPolicyFrames; } + virtual uint8_t getDialogToken() const { return dialogToken; } + virtual uint64_t getTransactionId() const { return transactionId; } + virtual simtime_t getAddbaResponseDeadline() const { return addbaResponseDeadline; } virtual void setBufferSize(int bufferSize) { this->bufferSize = bufferSize; } - virtual void setIsAddbaResponseReceived(bool isAddbaResponseReceived) { this->isAddbaResponseReceived = isAddbaResponseReceived; } + virtual void setIsAddbaResponseReceived(bool isAddbaResponseReceived) { state = isAddbaResponseReceived ? OriginatorBlockAckAgreementState::ESTABLISHED : OriginatorBlockAckAgreementState::PENDING; } virtual void setIsAddbaRequestSent(bool isAddbaRequestSent) { this->isAddbaRequestSent = isAddbaRequestSent; } virtual void setIsAMsduSupported(bool isAMsduSupported) { this->isAMsduSupported = isAMsduSupported; } virtual void setIsDelayedBlockAckPolicySupported(bool isDelayedBlockAckPolicySupported) { this->isDelayedBlockAckPolicySupported = isDelayedBlockAckPolicySupported; } virtual void setBlockAckTimeoutValue(const simtime_t blockAckTimeoutValue) { this->blockAckTimeoutValue = blockAckTimeoutValue; } + virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } virtual void baPolicyFrameSent() { numSentBaPolicyFrames++; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2f11dc365db..2d91fdaa91c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -7,18 +7,30 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include + #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { namespace ieee80211 { -void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest) +void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest, uint64_t transactionId) { - OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0); + ASSERT(addbaRequest->getDialogToken() != 0); + OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0, addbaRequest->getDialogToken(), transactionId); auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid()); blockAckAgreements[agreementId] = blockAckAgreement; } +uint8_t OriginatorBlockAckAgreementHandler::allocateDialogToken() +{ + auto dialogToken = nextDialogToken; + nextDialogToken = nextDialogToken == 255 ? 1 : nextDialogToken + 1; + return dialogToken; +} + simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() { simtime_t earliestTime = SIMTIME_MAX; @@ -33,6 +45,57 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() return earliestTime; } +simtime_t OriginatorBlockAckAgreementHandler::computeEarliestAddbaResponseDeadline() const +{ + simtime_t earliestDeadline = SIMTIME_MAX; + for (const auto& entry : blockAckAgreements) { + auto agreement = entry.second; + if (agreement->isPending() && agreement->getIsAddbaRequestSent()) { + ASSERT(agreement->getAddbaResponseDeadline() >= 0); + earliestDeadline = std::min(earliestDeadline, agreement->getAddbaResponseDeadline()); + } + } + return earliestDeadline; +} + +simtime_t OriginatorBlockAckAgreementHandler::getAddbaResponseTimeout(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) const +{ + auto timeout = blockAckAgreementPolicy->getAddbaResponseTimeout(); + if (timeout <= 0) + throw cRuntimeError("ADDBA response timeout must be greater than zero"); + return timeout; +} + +void OriginatorBlockAckAgreementHandler::recordAddbaFailure(MacAddress receiverAddr, Tid tid, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) +{ + auto retryBackoff = blockAckAgreementPolicy->computeAddbaRetryBackoff(); + if (retryBackoff < 0) + throw cRuntimeError("ADDBA retry backoff must not be negative"); + addbaRetryDeadlines[std::make_pair(receiverAddr, tid)] = simTime() + retryBackoff; +} + +void OriginatorBlockAckAgreementHandler::addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +{ + auto now = simTime(); + std::vector expiredTransactionIds; + for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) { + auto agreement = it->second; + if (agreement->isPending() && agreement->getIsAddbaRequestSent() && agreement->getAddbaResponseDeadline() <= now) { + EV_INFO << "ADDBA transaction timeout for receiver=" << agreement->getReceiverAddr() << " tid=" << (int)agreement->getTid() << endl; + auto transactionId = agreement->getTransactionId(); + recordAddbaFailure(agreement->getReceiverAddr(), agreement->getTid(), blockAckAgreementPolicy); + it = blockAckAgreements.erase(it); + delete agreement; + expiredTransactionIds.push_back(transactionId); + } + else + it++; + } + for (auto transactionId : expiredTransactionIds) + callback->cancelAddbaTransaction(transactionId, nullptr); + scheduleAddbaResponseTimer(callback); +} + void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the @@ -57,6 +120,9 @@ const Ptr OriginatorBlockAckAgreementHandler::buildAddbaR { auto addbaRequest = makeShared(); addbaRequest->setReceiverAddress(receiverAddr); + // IEEE Std 802.11-2024, 9.6.4.2: a solicited ADDBA Request uses a nonzero + // Dialog Token, and the corresponding response copies that token. + addbaRequest->setDialogToken(allocateDialogToken()); addbaRequest->setTid(tid); addbaRequest->setAMsduSupported(blockAckAgreementPolicy->isMsduSupported()); addbaRequest->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue()); @@ -92,6 +158,11 @@ void OriginatorBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreem callback->scheduleInactivityTimer(earliestExpirationTime); } +void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) +{ + callback->scheduleAddbaResponseTimer(computeEarliestAddbaResponseDeadline()); +} + OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid) { auto agreementId = std::make_pair(receiverAddr, tid); @@ -99,6 +170,12 @@ OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(Ma return it != blockAckAgreements.end() ? it->second : nullptr; } +bool OriginatorBlockAckAgreementHandler::isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const +{ + auto it = blockAckAgreements.find(std::make_pair(receiverAddr, tid)); + return it != blockAckAgreements.end() && it->second->isPending(); +} + const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode) { auto delba = makeShared(); @@ -110,37 +187,102 @@ const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddr return delba; } -void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::removeAgreement(MacAddress originatorAddr, Tid tid) { auto agreementId = std::make_pair(originatorAddr, tid); auto it = blockAckAgreements.find(agreementId); if (it != blockAckAgreements.end()) { - OriginatorBlockAckAgreement *agreement = it->second; + auto agreement = it->second; blockAckAgreements.erase(it); - delete agreement; + return agreement; } + return nullptr; } -void OriginatorBlockAckAgreementHandler::processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) { - auto agreement = getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()); - if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr) { - auto addbaReq = buildAddbaRequest(dataHeader->getReceiverAddress(), dataHeader->getTid(), dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); - createAgreement(addbaReq); + delete removeAgreement(originatorAddr, tid); +} + +uint64_t OriginatorBlockAckAgreementHandler::processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) +{ + // IEEE Std 802.11-2024, Table 9-466: the Starting Sequence Number identifies + // the first or next MSDU/A-MSDU sent under the agreement. Wait until the + // final fragment is acknowledged so no remaining fragment precedes the SSN. + if (dataHeader->getMoreFragments()) + return 0; + auto receiverAddr = dataHeader->getReceiverAddress(); + auto tid = dataHeader->getTid(); + auto agreementId = std::make_pair(receiverAddr, tid); + auto agreement = getAgreement(receiverAddr, tid); + auto retryIt = addbaRetryDeadlines.find(agreementId); + bool retryAllowed = retryIt == addbaRetryDeadlines.end() || retryIt->second <= simTime(); + uint64_t obsoleteTeardownTransactionId = 0; + if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr && retryAllowed) { + if (retryIt != addbaRetryDeadlines.end()) + addbaRetryDeadlines.erase(retryIt); + // A replacement agreement makes an older best-effort DELBA stale. Drop + // its eligibility instead of waiting indefinitely for a queue callback + // that every custom packet provider may not deliver. + auto teardownIt = pendingTeardownTransactionIds.find(agreementId); + if (teardownIt != pendingTeardownTransactionIds.end()) { + obsoleteTeardownTransactionId = teardownIt->second; + pendingTeardownTransactionIds.erase(teardownIt); + } + // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.2: Normal Ack data is + // permitted before an agreement exists, and the requested SSN starts + // after the acknowledged trigger MPDU. + auto addbaReq = buildAddbaRequest(receiverAddr, tid, dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); + auto transactionId = nextTransactionId++; + createAgreement(addbaReq, transactionId); auto addbaPacket = new Packet("AddbaReq", addbaReq); - callback->processMgmtFrame(addbaPacket, addbaReq); + addbaPacket->addTag()->setTransactionId(transactionId); + procedureCallback->processMgmtFrame(addbaPacket, addbaReq); } + return obsoleteTeardownTransactionId; } -void OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { + OriginatorBlockAckAgreementResponse response; auto agreement = getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement)) { + // IEEE Std 802.11-2024, 11.5.2.2: only a successful response matching the + // outstanding peer, TID, and Dialog Token establishes the agreement. + if (agreement == nullptr || !agreement->isPending() || !agreement->getIsAddbaRequestSent() || agreement->getDialogToken() != addbaResp->getDialogToken()) + return response; + bool acceptedByLocalPolicy = addbaResp->getStatusCode() == 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement); + if (addbaResp->getStatusCode() == 0) { + auto transactionId = agreement->getTransactionId(); updateAgreement(agreement, addbaResp); + if (acceptedByLocalPolicy) + addbaRetryDeadlines.erase(std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())); + else + recordAddbaFailure(addbaResp->getTransmitterAddress(), addbaResp->getTid(), blockAckAgreementPolicy); scheduleInactivityTimer(callback); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, nullptr); + if (!acceptedByLocalPolicy) { + // IEEE Std 802.11-2024, 10.25.2 Note 3: delete a successful + // agreement rejected by local policy and continue with Normal Ack. + response.terminatedAgreement.reset(removeAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid())); + if (response.terminatedAgreement == nullptr) + throw cRuntimeError("Cannot terminate locally vetoed Block Ack agreement"); + response.teardownDelba = buildDelba(addbaResp->getTransmitterAddress(), addbaResp->getTid(), RC_END_BA); + pendingTeardownTransactionIds[std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())] = transactionId; + response.teardownTransactionId = transactionId; + scheduleInactivityTimer(callback); + } + else + response.establishedAgreement = agreement; + return response; } else { - // TODO send a new one? + auto transactionId = agreement->getTransactionId(); + recordAddbaFailure(addbaResp->getTransmitterAddress(), addbaResp->getTid(), blockAckAgreementPolicy); + terminateAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, nullptr); + return response; } } @@ -152,24 +294,122 @@ void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgree agreement->calculateExpirationTime(); } -void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(const Ptr& addbaReq) +bool OriginatorBlockAckAgreementHandler::isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const +{ + auto it = blockAckAgreements.find(std::make_pair(addbaReq->getReceiverAddress(), addbaReq->getTid())); + auto transactionTag = packet->findTag(); + return it != blockAckAgreements.end() && it->second->isPending() && transactionTag != nullptr && + it->second->getDialogToken() == addbaReq->getDialogToken() && it->second->getTransactionId() == transactionTag->getTransactionId(); +} + +bool OriginatorBlockAckAgreementHandler::isDelbaPending(const Packet *packet, const Ptr& delba) const +{ + if (!delba->getInitiator()) + return true; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return true; + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId(); +} + +void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { auto agreement = getAgreement(addbaReq->getReceiverAddress(), addbaReq->getTid()); - if (agreement) + if (isAddbaRequestPending(packet, addbaReq) && !addbaReq->getMoreFragments() && !agreement->getIsAddbaRequestSent()) { + agreement->setAddbaResponseDeadline(simTime() + getAddbaResponseTimeout(blockAckAgreementPolicy)); agreement->setIsAddbaRequestSent(true); - else - throw cRuntimeError("Block Ack Agreement should have already been added"); + scheduleAddbaResponseTimer(callback); + } + else if (!isAddbaRequestPending(packet, addbaReq)) + EV_WARN << "Ignoring stale transmitted ADDBA Request for receiver=" << addbaReq->getReceiverAddress() << " tid=" << (int)addbaReq->getTid() << " dialogToken=" << (int)addbaReq->getDialogToken() << endl; } -void OriginatorBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +void OriginatorBlockAckAgreementHandler::processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + if (isAddbaRequestPending(packet, addbaReq)) { + auto transactionId = packet->getTag()->getTransactionId(); + recordAddbaFailure(addbaReq->getReceiverAddress(), addbaReq->getTid(), blockAckAgreementPolicy); + terminateAgreement(addbaReq->getReceiverAddress(), addbaReq->getTid()); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, packet); + } +} + +std::unique_ptr OriginatorBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = packet->peekAtFront(); + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr) { + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + return nullptr; + // IEEE Std 802.11-2024, 11.5.3.2: teardown is performed by + // transmitting DELBA. IEEE Std 802.11-2024, 10.23.2.12.1 and + // 10.3.4.4 require unsuccessful MMPDU attempts to be retried until + // success or the applicable retry limit. Keep the local transaction + // live across ordinary MAC retries and retire it only after the final + // fragment is acknowledged or the frame is terminally aborted. + return nullptr; + } + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); + auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; +} + +bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = packet->peekAtFront(); + if (!delba->getInitiator() || delba->getMoreFragments()) + return false; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return false; + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + return false; + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelAddbaTransaction(transactionId, packet); + return true; } -void OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) +bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getTransmitterAddress(), delba->getTid()); + auto delba = packet->peekAtFront(); + if (!delba->getInitiator()) + return false; + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr) { + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId()) { + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelAddbaTransaction(transactionId, packet); + return true; + } + } + return false; +} + +std::unique_ptr OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +{ + if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { + auto agreement = getAgreement(delba->getTransmitterAddress(), delba->getTid()); + bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); + auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; + } + return nullptr; } OriginatorBlockAckAgreementHandler::~OriginatorBlockAckAgreementHandler() diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4a449d17da3..9933ccadf92 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -21,27 +21,44 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg { protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; + std::map, simtime_t> addbaRetryDeadlines; + std::map, uint64_t> pendingTeardownTransactionIds; + uint8_t nextDialogToken = 1; + uint64_t nextTransactionId = 1; protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); - virtual void createAgreement(const Ptr& addbaRequest); + virtual uint8_t allocateDialogToken(); + virtual void createAgreement(const Ptr& addbaRequest, uint64_t transactionId); virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); + virtual OriginatorBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); virtual simtime_t computeEarliestExpirationTime(); + virtual simtime_t computeEarliestAddbaResponseDeadline() const; virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); + virtual void scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback); + virtual simtime_t getAddbaResponseTimeout(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) const; + virtual void recordAddbaFailure(MacAddress receiverAddr, Tid tid, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); public: virtual ~OriginatorBlockAckAgreementHandler(); - virtual void processTransmittedAddbaReq(const Ptr& addbaReq) override; - virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; + virtual void processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual void processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual uint64_t processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) override; virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) override; - virtual void processTransmittedDelba(const Ptr& delba) override; + virtual OriginatorBlockAckAgreementResponse processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) override; + virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const override; + virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const override; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 5a40aeb40cc..740d18a0033 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -24,17 +24,16 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage) aMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); - // TODO addbaFailureTimeout = par("addbaFailureTimeout"); + addbaResponseTimeout = par("addbaResponseTimeout"); + addbaRetryBackoff = par("addbaRetryBackoff"); + if (addbaResponseTimeout <= 0) + throw cRuntimeError("addbaResponseTimeout must be greater than zero"); + if (addbaRetryBackoff < 0) + throw cRuntimeError("addbaRetryBackoff must not be negative"); WATCH(blockAckReqThreshold); } } -simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const -{ - // TODO ADDBAFailureTimeout -- 6.3.29.2.2 Semantics of the service primitive - throw cRuntimeError("Unimplemented"); -} - bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const Ptr& header) { return ackPolicy->isBlockAckPolicyEligibleFrame(packet, header); @@ -42,8 +41,7 @@ bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const P bool OriginatorBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) { - ASSERT(agreement); - return true; + return agreement != nullptr && addbaResp->getStatusCode() == 0; } bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index 88fadd48377..04c67ec3af8 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -25,7 +25,8 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetModuleBase, pub bool aMsduSupported = false; int maximumAllowedBufferSize = -1; simtime_t blockAckTimeoutValue = -1; - simtime_t addbaFailureTimeout = -1; + simtime_t addbaResponseTimeout = -1; + simtime_t addbaRetryBackoff = -1; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } @@ -36,7 +37,8 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetModuleBase, pub virtual bool isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) override; virtual bool isDelbaAccepted(const Ptr& delba) override; - virtual simtime_t computeAddbaFailureTimeout() const override; + virtual simtime_t getAddbaResponseTimeout() const override { return addbaResponseTimeout; } + virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } virtual bool isMsduSupported() const override { return aMsduSupported; } virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index 68ee746d154..8fa02193718 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -24,6 +24,8 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator + double addbaResponseTimeout @unit(s) = default(1s); // Model response-wait timeout after the ADDBA Request is transmitted + double addbaRetryBackoff @unit(s) = default(1s); // Model retry suppression after a failed or discarded ADDBA transaction; not an IEEE 802.11 timer @display("i=block/control"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 6219ac77140..3e599bb4a48 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -106,6 +106,8 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR { auto addbaResponse = makeShared(); addbaResponse->setReceiverAddress(addbaRequest->getTransmitterAddress()); + // IEEE Std 802.11-2024, 9.6.4.2 and 11.5.2.3: echo the request identity. + addbaResponse->setDialogToken(addbaRequest->getDialogToken()); // The Block Ack Policy subfield is set to 1 for immediate Block Ack and 0 for delayed Block Ack. Tid tid = addbaRequest->getTid(); addbaResponse->setTid(tid); diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h index c481e9a82c8..e56ca2ffb6f 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h @@ -38,6 +38,7 @@ class INET_API Edca : public SimpleModule virtual ~Edca(); virtual AccessCategory classifyFrame(const Ptr& header); + virtual int getNumEdcafs() const { return numEdcafs; } virtual Edcaf *getEdcaf(AccessCategory ac) const { return edcafs[ac]; } virtual Edcaf *getChannelOwner(); virtual std::vector getInternallyCollidedEdcafs(); diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cc4a7b86b92..d83044b63a2 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -11,6 +11,9 @@ #include "inet/common/INETDefs.h" namespace inet { + +class Packet; + namespace ieee80211 { class INET_API IBlockAckAgreementHandlerCallback @@ -19,6 +22,8 @@ class INET_API IBlockAckAgreementHandlerCallback virtual ~IBlockAckAgreementHandlerCallback() {} virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h index 8260a067cef..5023ad8023b 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h @@ -8,6 +8,8 @@ #ifndef __INET_IMSDUAGGREGATIONPOLICY_H #define __INET_IMSDUAGGREGATIONPOLICY_H +#include + #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/queueing/contract/IPacketQueue.h" @@ -19,7 +21,9 @@ class INET_API IMsduAggregationPolicy public: virtual ~IMsduAggregationPolicy() {} - virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue) = 0; + // A non-null result is caller-owned and must contain at least two unique, + // discoverable, eligible frames with candidate as its first element. + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 4a01e89b6c3..068c90f7d35 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -8,9 +8,12 @@ #ifndef __INET_IORIGINATORBLOCKACKAGREEMENTHANDLER_H #define __INET_IORIGINATORBLOCKACKAGREEMENTHANDLER_H +#include + #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/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" #include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" @@ -19,20 +22,41 @@ namespace inet { namespace ieee80211 { +struct INET_API OriginatorBlockAckAgreementResponse +{ + // Borrowed from the handler; valid while the established agreement remains installed. + OriginatorBlockAckAgreement *establishedAgreement = nullptr; + // Owns the agreement that was established and immediately terminated after a local veto. + std::unique_ptr terminatedAgreement; + Ptr teardownDelba; + uint64_t teardownTransactionId = 0; +}; + class INET_API IOriginatorBlockAckAgreementHandler { public: virtual ~IOriginatorBlockAckAgreementHandler() {} virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processTransmittedAddbaReq(const Ptr& addbaReq) = 0; - virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; - virtual void processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual void processTransmittedDelba(const Ptr& delba) = 0; + virtual void processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + // Returns the transaction identity of an obsolete teardown whose packets + // must be cancelled by the caller, or 0 when there is none. + virtual uint64_t processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) = 0; + virtual OriginatorBlockAckAgreementResponse processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + // Returns true when the packet completed or aborted its tagged teardown + // transaction and sibling packets were cancelled through the callback. + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) = 0; + virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const = 0; + virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const = 0; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index 40ea82aa95e..facc4d5791c 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -26,7 +26,8 @@ class INET_API IOriginatorBlockAckAgreementPolicy virtual bool isDelbaAccepted(const Ptr& delba) = 0; virtual bool isMsduSupported() const = 0; - virtual simtime_t computeAddbaFailureTimeout() const = 0; + virtual simtime_t getAddbaResponseTimeout() const = 0; + virtual simtime_t computeAddbaRetryBackoff() const = 0; virtual simtime_t getBlockAckTimeoutValue() const = 0; virtual bool isDelayedAckPolicySupported() const = 0; virtual int getMaximumAllowedBufferSize() const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h index 4e66ec6c190..526275fc83e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h @@ -8,6 +8,8 @@ #ifndef __INET_IORIGINATORMACDATASERVICE_H #define __INET_IORIGINATORMACDATASERVICE_H +#include + #include "inet/queueing/contract/IPacketQueue.h" namespace inet { @@ -15,6 +17,9 @@ namespace ieee80211 { class INET_API IOriginatorMacDataService { + public: + using FrameEligibilityFunction = std::function; + public: static simsignal_t packetFragmentedSignal; static simsignal_t packetAggregatedSignal; @@ -22,6 +27,9 @@ class INET_API IOriginatorMacDataService public: virtual ~IOriginatorMacDataService() {} + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) = 0; + virtual bool isFrameEligible(const Packet *packet) const = 0; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const = 0; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 85835ebd822..a660a3750f3 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -82,6 +82,12 @@ void Dcf::channelGranted(IChannelAccess *channelAccess) Enter_Method("channelGranted"); ASSERT(this->channelAccess == channelAccess); if (!frameSequenceHandler->isSequenceRunning()) { + if (this->channelAccess->getInProgressFrames()->getFrameToTransmit() == nullptr) { + EV_DETAIL << "Releasing channel because no frame is available.\n"; + channelAccess->releaseChannel(this); + mac->sendDownPendingRadioConfigMsg(); + return; + } frameSequenceHandler->startFrameSequence(new DcfFs(), buildContext(), this); emit(IFrameSequenceHandler::frameSequenceStartedSignal, frameSequenceHandler->getContext()); } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 6b13cb7acb1..b97f73e7a61 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -13,9 +13,11 @@ #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" @@ -40,6 +42,7 @@ void Hcf::initialize(int stage) mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); startRxTimer = new cMessage("startRxTimeout"); inactivityTimer = new cMessage("blockAckInactivityTimer"); + addbaResponseTimer = new cMessage("addbaResponseTimer"); edca = check_and_cast(getSubmodule("edca")); hcca = check_and_cast(getSubmodule("hcca")); tx = check_and_cast(getModuleByPath(par("txModule"))); @@ -65,14 +68,119 @@ void Hcf::initialize(int stage) originatorBlockAckAgreementHandler = new OriginatorBlockAckAgreementHandler(); originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); + originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { + if (auto addbaReq = dynamicPtrCast(packet->peekAtFront())) + return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); + if (auto delba = dynamicPtrCast(packet->peekAtFront())) + return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + auto dataHeader = dynamicPtrCast(packet->peekAtFront()); + // Hold this peer/TID while its ADDBA response is pending so no + // already-sequenced MPDU can precede the advertised SSN. The + // response timeout starts only after the request is transmitted. + return dataHeader == nullptr || dataHeader->getType() != ST_DATA_WITH_QOS || (!originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())); + }); } } else if (stage == INITSTAGE_LAST) { // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. - for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) - check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto pendingQueue = edca->getEdcaf(AccessCategory(ac))->getPendingQueue(); + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + } + rebuildPendingFrameEligibility(); + } +} + +void Hcf::trackPendingFrame(Packet *packet, AccessCategory accessCategory) +{ + untrackPendingFrame(packet); + bool eligible = originatorDataService->isFrameEligible(packet); + pendingFrameEligibility.emplace(packet, PendingFrameEligibility { accessCategory, eligible }); + if (eligible) + numEligiblePendingFrames[accessCategory]++; +} + +void Hcf::untrackPendingFrame(const Packet *packet) +{ + auto it = pendingFrameEligibility.find(packet); + if (it != pendingFrameEligibility.end()) { + if (it->second.eligible) { + ASSERT(numEligiblePendingFrames[it->second.accessCategory] > 0); + numEligiblePendingFrames[it->second.accessCategory]--; + } + pendingFrameEligibility.erase(it); + } +} + +void Hcf::rebuildPendingFrameEligibility() +{ + pendingFrameEligibility.clear(); + numEligiblePendingFrames.fill(0); + int numPendingFrames = 0; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto accessCategory = AccessCategory(ac); + auto pendingQueue = edca->getEdcaf(accessCategory)->getPendingQueue(); + for (int i = 0; i < pendingQueue->getNumPackets(); i++) { + trackPendingFrame(pendingQueue->getPacket(i), accessCategory); + numPendingFrames++; + } } + ASSERT((int)pendingFrameEligibility.size() == numPendingFrames); +} + +bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) +{ + if (originatorBlockAckAgreementHandler) { + auto addbaReq = dynamicPtrCast(packet->peekAtFront()); + if (addbaReq != nullptr && originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq)) { + originatorBlockAckAgreementHandler->processDroppedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + return true; + } + } + return false; +} + +bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) +{ + if (originatorBlockAckAgreementHandler) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { + rebuildPendingFrameEligibility(); + return true; + } + } + return false; +} + +void Hcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) +{ + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } +} + +void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + untrackPendingFrame(packet); + bool shouldResume = false; + // HCF treats explicit REMOVED notifications as terminal transaction + // disposal; code relocating a packet must use dequeuePacket(). + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) { + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } + shouldResume |= processDroppedBlockAckSetupFrame(packet); + shouldResume |= processDroppedBlockAckTeardownFrame(packet); + } + if (shouldResume) + resumeEligibleChannelAccess(); } std::string Hcf::getFrameSequenceInfo() const @@ -109,6 +217,15 @@ void Hcf::handleMessage(cMessage *msg) else throw cRuntimeError("Unknown event"); } + else if (msg == addbaResponseTimer) { + if (originatorBlockAckAgreementHandler) { + originatorBlockAckAgreementHandler->addbaResponseTimeoutExpired(originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } + else + throw cRuntimeError("Unknown event"); + } else throw cRuntimeError("Unknown msg type"); } @@ -146,8 +263,9 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); + trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (!pendingQueue->isEmpty()) { + if (hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -156,23 +274,22 @@ void Hcf::processUpperFrame(Packet *packet, const PtrisSubscribed(signal, this)) - handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); - } -} - -void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) -{ - Enter_Method("handlePacketRemoved"); - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + if (frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto transmitStep = dynamic_cast(context->getStep(i)); + if (transmitStep != nullptr && transmitStep->getFrameToTransmit() == packet) + return true; + auto rtsTransmitStep = dynamic_cast(transmitStep); + if (rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet) + return true; } + return false; } void Hcf::scheduleStartRxTimer(simtime_t timeout) @@ -187,6 +304,58 @@ void Hcf::scheduleInactivityTimer(simtime_t timeout) rescheduleAfter(timeout, inactivityTimer); } +void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) +{ + Enter_Method("scheduleAddbaResponseTimer"); + if (deadline == SIMTIME_MAX) { + if (addbaResponseTimer->isScheduled()) + cancelEvent(addbaResponseTimer); + } + else + rescheduleAt(deadline, addbaResponseTimer); +} + +void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelAddbaTransaction"); + // Frames borrowed by the active sequence cannot be removed here. The + // sequence's failure paths detect their now-stale transaction and discard them. + auto belongsToTransaction = [this, transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && !isPacketReferencedByCurrentFrameSequence(packet) && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + } +} + void Hcf::processLowerFrame(Packet *packet, const Ptr& header) { Enter_Method("processLowerFrame(%s)", packet->getName()); @@ -231,13 +400,22 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) if (edcaf) { AccessCategory ac = edcaf->getAccessCategory(); EV_DETAIL << "Channel access granted to the " << printAccessCategory(ac) << " queue" << std::endl; - edcaf->getTxopProcedure()->startTxop(ac); auto internallyCollidedEdcafs = edca->getInternallyCollidedEdcafs(); if (internallyCollidedEdcafs.size() > 0) { EV_INFO << "Internal collision happened with the following queues:" << std::endl; - handleInternalCollision(internallyCollidedEdcafs); - emit(edcaCollisionDetectedSignal, (unsigned long)internallyCollidedEdcafs.size()); + // IEEE Std 802.11-2024, 10.23.2.4: an EDCAF with no eligible + // frame has no collision recovery action to perform. + auto handledCollisions = handleInternalCollision(internallyCollidedEdcafs); + if (handledCollisions > 0) + emit(edcaCollisionDetectedSignal, (unsigned long)handledCollisions); } + if (!hasFrameToTransmit(ac)) { + EV_DETAIL << "Releasing channel because no eligible frame is available.\n"; + edcaf->releaseChannel(this); + mac->sendDownPendingRadioConfigMsg(); + return; + } + edcaf->getTxopProcedure()->startTxop(ac); startFrameSequence(ac); } else @@ -257,12 +435,18 @@ void Hcf::startFrameSequence(AccessCategory ac) emit(IFrameSequenceHandler::frameSequenceStartedSignal, frameSequenceHandler->getContext()); } -void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) +int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) { + int handledCollisions = 0; for (auto edcaf : internallyCollidedEdcafs) { AccessCategory ac = edcaf->getAccessCategory(); auto dataRecoveryProcedure = edcaf->getRecoveryProcedure(); Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); + if (internallyCollidedFrame == nullptr) { + EV_DETAIL << "Ignoring internal collision because no eligible frame is available for " << printAccessCategory(ac) << ".\n"; + continue; + } + handledCollisions++; auto internallyCollidedHeader = internallyCollidedFrame->peekAtFront(); EV_INFO << printAccessCategory(ac) << " (" << internallyCollidedFrame->getName() << ")" << endl; bool retryLimitReached = false; @@ -284,6 +468,8 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(internallyCollidedFrame, mgmtHeader); else ; // TODO + NonQoSDataFrame edcaf->getInProgressFrames()->dropFrame(internallyCollidedFrame); + processDroppedBlockAckSetupFrame(internallyCollidedFrame); + processDroppedBlockAckTeardownFrame(internallyCollidedFrame); edcaf->getAckHandler()->dropFrame(internallyCollidedHeader); PacketDropDetails details; details.setReason(RETRY_LIMIT_REACHED); @@ -301,6 +487,7 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else edcaf->requestChannel(this); } + return handledCollisions; } /* @@ -315,12 +502,11 @@ void Hcf::frameSequenceFinished() emit(IFrameSequenceHandler::frameSequenceFinishedSignal, frameSequenceHandler->getContext()); auto edcaf = edca->getChannelOwner(); if (edcaf) { - bool startContention = hasFrameToTransmit(); // TODO outstanding frame edcaf->releaseChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review edcaf->getTxopProcedure()->endTxop(); - if (startContention) - edcaf->requestChannel(this); + // Agreement transitions may have made frames in any AC eligible. + requestEligibleChannelAccess(); } else if (hcca->isOwning()) { hcca->releaseChannel(this); @@ -376,9 +562,24 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { - originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); - auto agreement = originatorBlockAckAgreementHandler->getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); - emit(blockAckAgreementAddedSignal, agreement); + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + auto response = originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); + if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid())) + rebuildPendingFrameEligibility(); + if (response.teardownDelba != nullptr && (response.terminatedAgreement == nullptr || response.teardownTransactionId == 0)) + throw cRuntimeError("Invalid locally vetoed ADDBA response outcome"); + if (response.establishedAgreement != nullptr) + emit(blockAckAgreementAddedSignal, response.establishedAgreement); + if (response.terminatedAgreement != nullptr) { + emit(blockAckAgreementAddedSignal, response.terminatedAgreement.get()); + emit(blockAckAgreementDeletedSignal, response.terminatedAgreement.get()); + } + if (response.teardownDelba != nullptr) { + auto delbaPacket = new Packet("Delba", response.teardownDelba); + delbaPacket->addTag()->setTransactionId(response.teardownTransactionId); + processMgmtFrame(delbaPacket, response.teardownDelba); + } + resumeEligibleChannelAccess(); } else if (auto delba = dynamicPtrCast(header)) { if (delba->getInitiator()) { @@ -387,9 +588,13 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrprocessReceivedDelba(delba, recipientBlockAckAgreementPolicy); } else { - auto agreement = originatorBlockAckAgreementHandler->getAgreement(delba->getReceiverAddress(), delba->getTid()); - emit(blockAckAgreementDeletedSignal, agreement); - originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy); + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy, this); + if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid())) + rebuildPendingFrameEligibility(); + if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + resumeEligibleChannelAccess(); } } else @@ -430,24 +635,35 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else throw cRuntimeError("Unknown frame"); // TODO QoSDataFrame, NonQoSDataFrame - if (retryLimitReached) { - if (auto dataHeader = dynamicPtrCast(protectedHeader)) - edcaf->getRecoveryProcedure()->retryLimitReached(packet, dataHeader); - else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) - edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(packet, mgmtHeader); - else ; // TODO nonqos data + auto addbaRequest = dynamicPtrCast(protectedHeader); + bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaRequest); + if (retryLimitReached || staleAddbaRequest) { + if (retryLimitReached) { + if (auto dataHeader = dynamicPtrCast(protectedHeader)) + edcaf->getRecoveryProcedure()->retryLimitReached(packet, dataHeader); + else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) + edca->getMgmtAndNonQoSRecoveryProcedure()->rtsFrameRetryLimitReached(packet, mgmtHeader); + else ; // TODO nonqos data + } + else + edca->getMgmtAndNonQoSRecoveryProcedure()->discardRtsFrame(addbaRequest); edcaf->getInProgressFrames()->dropFrame(packet); + processDroppedBlockAckSetupFrame(packet); + processDroppedBlockAckTeardownFrame(packet); edcaf->getAckHandler()->dropFrame(protectedHeader); - EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << ", because retry limit is reached.\n"; + EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << (retryLimitReached ? ", because retry limit is reached.\n" : ", because its ADDBA transaction is no longer pending.\n"); PacketDropDetails details; - details.setReason(RETRY_LIMIT_REACHED); - details.setLimit(-1); // TODO + details.setReason(retryLimitReached ? RETRY_LIMIT_REACHED : OTHER_PACKET_DROP); + if (retryLimitReached) + details.setLimit(-1); // TODO emit(packetDroppedSignal, packet, &details); - emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + if (retryLimitReached) { + emit(linkBrokenSignal, packet); + if (dynamicPtrCast(protectedHeader)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } } @@ -472,7 +688,7 @@ void Hcf::originatorProcessTransmittedFrame(Packet *packet) else if (auto dataHeader = dynamicPtrCast(transmittedHeader)) originatorProcessTransmittedDataFrame(packet, dataHeader, ac); else if (auto mgmtHeader = dynamicPtrCast(transmittedHeader)) - originatorProcessTransmittedManagementFrame(mgmtHeader, ac); + originatorProcessTransmittedManagementFrame(packet, mgmtHeader, ac); else // TODO Ieee80211ControlFrame originatorProcessTransmittedControlFrame(transmittedHeader, ac); } @@ -486,26 +702,30 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this); if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } -void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& mgmtHeader, AccessCategory ac) +void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr& mgmtHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); if (originatorAckPolicy->isAckNeeded(mgmtHeader)) edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(mgmtHeader); if (auto addbaReq = dynamicPtrCast(mgmtHeader)) { if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedAddbaReq(addbaReq); + originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } else if (auto addbaResp = dynamicPtrCast(mgmtHeader)) recipientBlockAckAgreementHandler->processTransmittedAddbaResp(addbaResp, this); else if (auto delba = dynamicPtrCast(mgmtHeader)) { - if (delba->getInitiator()) - originatorBlockAckAgreementHandler->processTransmittedDelba(delba); + if (delba->getInitiator()) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); + if (wasPending) + rebuildPendingFrameEligibility(); + if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + } else recipientBlockAckAgreementHandler->processTransmittedDelba(delba); } @@ -557,23 +777,34 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else throw cRuntimeError("Unknown frame"); // TODO qos, nonqos - if (retryLimitReached) { - if (auto dataHeader = dynamicPtrCast(failedHeader)) - edcaf->getRecoveryProcedure()->retryLimitReached(failedPacket, dataHeader); - else if (auto mgmtHeader = dynamicPtrCast(failedHeader)) - edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(failedPacket, mgmtHeader); + auto addbaRequest = dynamicPtrCast(failedHeader); + bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(failedPacket, addbaRequest); + if (retryLimitReached || staleAddbaRequest) { + if (retryLimitReached) { + if (auto dataHeader = dynamicPtrCast(failedHeader)) + edcaf->getRecoveryProcedure()->retryLimitReached(failedPacket, dataHeader); + else if (auto mgmtHeader = dynamicPtrCast(failedHeader)) + edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(failedPacket, mgmtHeader); + } + else + edca->getMgmtAndNonQoSRecoveryProcedure()->discardFrame(failedPacket, addbaRequest); edcaf->getInProgressFrames()->dropFrame(failedPacket); + processDroppedBlockAckSetupFrame(failedPacket); + processDroppedBlockAckTeardownFrame(failedPacket); edcaf->getAckHandler()->dropFrame(dynamicPtrCast(failedHeader)); - EV_INFO << "Dropping frame " << failedPacket->getName() << ", because retry limit is reached.\n"; + EV_INFO << "Dropping frame " << failedPacket->getName() << (retryLimitReached ? ", because retry limit is reached.\n" : ", because its ADDBA transaction is no longer pending.\n"); PacketDropDetails details; - details.setReason(RETRY_LIMIT_REACHED); - details.setLimit(-1); // TODO + details.setReason(retryLimitReached ? RETRY_LIMIT_REACHED : OTHER_PACKET_DROP); + if (retryLimitReached) + details.setLimit(-1); // TODO emit(packetDroppedSignal, failedPacket, &details); - emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + if (retryLimitReached) { + emit(linkBrokenSignal, failedPacket); + if (dynamicPtrCast(failedHeader)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + } } } else { @@ -639,6 +870,10 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); + if (auto delba = dynamicPtrCast(lastTransmittedHeader)) { + if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) + rebuildPendingFrameEligibility(); + } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { @@ -646,6 +881,16 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader)) { + if (originatorBlockAckAgreementHandler) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + auto obsoleteTeardownTransactionId = originatorBlockAckAgreementHandler->processAcknowledgedDataFrame(lastTransmittedPacket, dataHeader, originatorBlockAckAgreementPolicy, this); + if (obsoleteTeardownTransactionId != 0) + cancelAddbaTransaction(obsoleteTeardownTransactionId, nullptr); + if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) + rebuildPendingFrameEligibility(); + } + } } else if (auto blockAck = dynamicPtrCast(header)) { EV_INFO << "BasicBlockAck has arrived" << std::endl; @@ -680,7 +925,7 @@ bool Hcf::hasFrameToTransmit(AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return numEligiblePendingFrames[ac] != 0 || edcaf->getInProgressFrames()->hasEligibleInProgressFrames(); else throw cRuntimeError("Hcca is unimplemented"); } @@ -689,11 +934,26 @@ bool Hcf::hasFrameToTransmit() { auto edcaf = edca->getChannelOwner(); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return numEligiblePendingFrames[edcaf->getAccessCategory()] != 0 || edcaf->getInProgressFrames()->hasEligibleInProgressFrames(); else throw cRuntimeError("Hcca is unimplemented"); } +void Hcf::requestEligibleChannelAccess() +{ + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto accessCategory = AccessCategory(ac); + if (hasFrameToTransmit(accessCategory)) + edca->requestChannelAccess(accessCategory, this); + } +} + +void Hcf::resumeEligibleChannelAccess() +{ + if (edca->getChannelOwner() == nullptr && !frameSequenceHandler->isSequenceRunning()) + requestEligibleChannelAccess(); +} + void Hcf::sendUp(const std::vector& completeFrames) { for (auto frame : completeFrames) @@ -820,8 +1080,11 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + // Callback pointers are stored by child queues, which are destroyed with + // this compound module. Traversing edca here may reach deleted children. cancelAndDelete(startRxTimer); cancelAndDelete(inactivityTimer); + cancelAndDelete(addbaResponseTimer); delete recipientAckProcedure; delete ctsProcedure; delete rtsProcedure; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 10a7bc50c56..f475aa8a1de 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,6 +8,9 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#include +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" @@ -59,6 +62,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; + cMessage *addbaResponseTimer = nullptr; // Transmission and Reception IRx *rx = nullptr; @@ -97,6 +101,13 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Queues InProgressFrames *hccaInProgressFrame = nullptr; + struct PendingFrameEligibility { + AccessCategory accessCategory; + bool eligible; + }; + std::map pendingFrameEligibility; + std::array numEligiblePendingFrames = {}; + // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; @@ -111,12 +122,20 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); - void handleInternalCollision(std::vector internallyCollidedEdcafs); + int handleInternalCollision(std::vector internallyCollidedEdcafs); void sendUp(const std::vector& completeFrames); FrameSequenceContext *buildContext(AccessCategory ac); virtual bool hasFrameToTransmit(); virtual bool hasFrameToTransmit(AccessCategory ac); + virtual void requestEligibleChannelAccess(); + virtual void resumeEligibleChannelAccess(); + virtual bool processDroppedBlockAckSetupFrame(Packet *packet); + virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); + virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); + virtual void untrackPendingFrame(const Packet *packet); + virtual void rebuildPendingFrameEligibility(); virtual bool isReceptionInProgress(); // Recipient @@ -126,7 +145,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); // Originator - virtual void originatorProcessTransmittedManagementFrame(const Ptr& mgmtHeader, AccessCategory ac); + virtual void originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr& mgmtHeader, AccessCategory ac); virtual void originatorProcessTransmittedControlFrame(const Ptr& controlHeader, AccessCategory ac); virtual void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, AccessCategory ac); virtual void originatorProcessReceivedManagementFrame(const Ptr& header, const Ptr& lastTransmittedHeader, AccessCategory ac); @@ -161,6 +180,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; + virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index fc81d4d863c..ee9e298570b 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -13,6 +13,14 @@ namespace inet { namespace ieee80211 { +static bool hasMoreTxOpsForFrame(RepeatingFs *frameSequence, FrameSequenceContext *context, Packet *nextFrameToTransmit) +{ + if (nextFrameToTransmit == nullptr) + return false; + const auto& nextHeader = nextFrameToTransmit->peekAtFront(); + return frameSequence->getCount() == 0 || (!nextHeader->getReceiverAddress().isMulticast() && context->getQoSContext()->txopProcedure->getRemaining() > 0); +} + HcfFs::HcfFs() : // G.3 EDCA and HCCA sequences // hcf-sequence = @@ -58,18 +66,16 @@ bool HcfFs::isSelfCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *con bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *context) { - bool hasFrameToTransmit = context->getInProgressFrames()->hasInProgressFrames(); - if (hasFrameToTransmit) { - auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - const auto& nextHeader = nextFrameToTransmit->peekAtFront(); - return frameSequence->getCount() == 0 || (!nextHeader->getReceiverAddress().isMulticast() && context->getQoSContext()->txopProcedure->getRemaining() > 0); - } - return false; + // This predicate is the TXOP continuation boundary, so it may materialize + // an eligible pending frame. Availability queries elsewhere remain pure. + auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + return hasMoreTxOpsForFrame(frameSequence, context, nextFrameToTransmit); } bool HcfFs::hasMoreTxOpsAndMulticast(RepeatingFs *frameSequence, FrameSequenceContext *context) { - return hasMoreTxOps(frameSequence, context) && context->getInProgressFrames()->getFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); + auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + return hasMoreTxOpsForFrame(frameSequence, context, nextFrameToTransmit) && nextFrameToTransmit->peekAtFront()->getReceiverAddress().isMulticast(); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc index 4074ff0004c..f982e263570 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc @@ -127,23 +127,34 @@ void NonQosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& header) +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + shortRetryCounter.erase(id); + longRetryCounter.erase(id); +} + +void NonQosRecoveryProcedure::discardRtsFrame(const Ptr& protectedHeader) +{ + auto id = SequenceControlField(protectedHeader->getSequenceNumber().get(), protectedHeader->getFragmentNumber()); + shortRetryCounter.erase(id); + longRetryCounter.erase(id); +} + // After dropping a frame because it reached its retry limit we need to clear the // retry counters // void NonQosRecoveryProcedure::retryLimitReached(Packet *packet, const Ptr& header) { EV_WARN << "Retry limit reached for " << *packet << ".\n"; - auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); - if (packet->getByteLength() >= rtsThreshold) { - auto it = longRetryCounter.find(id); - if (it != longRetryCounter.end()) - longRetryCounter.erase(it); - } - else { - auto it = shortRetryCounter.find(id); - if (it != shortRetryCounter.end()) - shortRetryCounter.erase(it); - } + discardFrame(packet, header); + emit(retryLimitReachedSignal, packet); +} + +void NonQosRecoveryProcedure::rtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader) +{ + EV_WARN << "RTS retry limit reached for " << *packet << ".\n"; + discardRtsFrame(protectedHeader); emit(retryLimitReachedSignal, packet); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h index f738e56dde6..135da9f5df7 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h @@ -64,7 +64,10 @@ class INET_API NonQosRecoveryProcedure : public SimpleModule, public IRecoveryPr virtual bool isRetryLimitReached(Packet *packet, const Ptr& failedHeader); virtual bool isRtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); + virtual void discardFrame(Packet *packet, const Ptr& header); + virtual void discardRtsFrame(const Ptr& protectedHeader); virtual void retryLimitReached(Packet *packet, const Ptr& header); + virtual void rtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); virtual int getLongRetryLimit() { return longRetryLimit; } virtual int getShortRetryLimit() { return shortRetryLimit; } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc index 12d6c5cd21e..6fae623f3a5 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/NonQoSSequenceNumberAssignment.h" @@ -32,21 +33,37 @@ std::vector *OriginatorMacDataService::fragmentIfNeeded(Packet *frame) auto fragmentSizes = fragmentationPolicy->computeFragmentSizes(frame); if (fragmentSizes.size() != 0) { emit(packetFragmentedSignal, frame); + auto transactionTag = frame->findTag(); + bool hasTransactionTag = transactionTag != nullptr; + auto transactionId = hasTransactionTag ? transactionTag->getTransactionId() : 0; auto fragmentFrames = fragmentation->fragmentFrame(frame, fragmentSizes); + if (hasTransactionTag) + for (auto fragment : *fragmentFrames) + fragment->addTagIfAbsent()->setTransactionId(transactionId); return fragmentFrames; } return nullptr; } +bool OriginatorMacDataService::isFrameEligible(const Packet *packet) const +{ + return !frameEligibilityFunction || frameEligibilityFunction(packet); +} + +bool OriginatorMacDataService::hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const +{ + return pendingQueue->findPacket([this](const Packet *packet) { return isFrameEligible(packet); }) != nullptr; +} + std::vector *OriginatorMacDataService::extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) { Enter_Method("extractFramesToTransmit"); - if (pendingQueue->isEmpty()) + auto packet = pendingQueue->dequeuePacket([this](const Packet *packet) { return isFrameEligible(packet); }); + if (packet == nullptr) return nullptr; else { // if (msduRateLimiting) // txRateLimitingIfNeeded(); - Packet *packet = pendingQueue->dequeuePacket(); take(packet); if (sequenceNumberAssignment) { auto frame = packet->removeAtFront(); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h index 2a75735b598..0c506e9cd28 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h @@ -30,6 +30,7 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ // MsduIntegrityAndProtection *msduIntegrityAndProtection = nullptr; IFragmentationPolicy *fragmentationPolicy = nullptr; IFragmentation *fragmentation = nullptr; + FrameEligibilityFunction frameEligibilityFunction; // MpduEncryptionAndIntegrity *mpduEncryptionAndIntegrity = nullptr; // MpduHeaderPlusFcs *mpduHeaderPlusFcs = nullptr; @@ -42,6 +43,9 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ public: virtual ~OriginatorMacDataService(); + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) override { this->frameEligibilityFunction = frameEligibilityFunction; } + virtual bool isFrameEligible(const Packet *packet) const override; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const override; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index f374452e933..7abfaed5a9c 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -8,9 +8,12 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include +#include +#include #include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" @@ -32,17 +35,53 @@ void OriginatorQosMacDataService::initialize() fragmentation = new Fragmentation(); } -Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue) +Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *candidate) { - auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue); + auto predicate = [this](const Packet *packet) { return isFrameEligible(packet); }; + std::unique_ptr> subframes(aMsduAggregationPolicy->computeAggregateFrames(pendingQueue, candidate, predicate)); if (subframes) { + if (subframes->size() < 2 || subframes->front() != candidate) + throw cRuntimeError("A-MSDU policy must return at least two frames with the selected candidate first"); + std::unordered_set uniqueFrames; for (auto subframe : *subframes) { - pendingQueue->removePacket(subframe); - take(subframe); + auto identity = [subframe](const Packet *packet) { return packet == subframe; }; + if (subframe == nullptr || !uniqueFrames.insert(subframe).second || pendingQueue->findPacket(identity) != subframe || !isFrameEligible(subframe)) + throw cRuntimeError("A-MSDU policy returned a frame that is unavailable, ineligible, or duplicated"); } - auto aggregatedFrame = aMsduAggregation->aggregateFrames(subframes); + struct AggregateFrameState { Tid tid; MacAddress receiver; MacAddress transmitter; MacAddress address3; MacAddress address4; int type; bool toDS; bool fromDS; b dataLength; b headerLength; b trailerLength; }; + std::vector states; + for (auto subframe : *subframes) { + auto dataHeader = dynamicPtrCast(subframe->peekAtFront()); + auto dataTrailer = subframe->peekAtBack(B(4)); + if (dataHeader == nullptr || dataTrailer == nullptr) + throw cRuntimeError("A-MSDU policy selected a frame without a valid data header/trailer"); + states.push_back({static_cast(dataHeader->getTid()), dataHeader->getReceiverAddress(), dataHeader->getTransmitterAddress(), dataHeader->getAddress3(), dataHeader->getAddress4(), dataHeader->getType(), dataHeader->getToDS(), dataHeader->getFromDS(), subframe->getDataLength(), dataHeader->getChunkLength(), dataTrailer->getChunkLength()}); + } + std::vector> extractedSubframes; + extractedSubframes.reserve(subframes->size()); + for (auto subframe : *subframes) { + auto dequeuedSubframe = pendingQueue->dequeuePacket([subframe](const Packet *packet) { return packet == subframe; }); + if (dequeuedSubframe != subframe) { + bool alreadyExtracted = std::any_of(extractedSubframes.begin(), extractedSubframes.end(), [dequeuedSubframe](const auto& extractedSubframe) { return extractedSubframe.get() == dequeuedSubframe; }); + if (dequeuedSubframe != nullptr && !alreadyExtracted) { + take(dequeuedSubframe); + extractedSubframes.emplace_back(dequeuedSubframe); + } + throw cRuntimeError("A-MSDU policy-selected subframe is no longer available in scheduling order"); + } + take(dequeuedSubframe); + extractedSubframes.emplace_back(dequeuedSubframe); + } + for (size_t i = 0; i < subframes->size(); i++) { + auto dataHeader = dynamicPtrCast((*subframes)[i]->peekAtFront()); + auto dataTrailer = (*subframes)[i]->peekAtBack(B(4)); + if (dataHeader == nullptr || dataTrailer == nullptr || dataHeader->getTid() != states[i].tid || dataHeader->getReceiverAddress() != states[i].receiver || dataHeader->getTransmitterAddress() != states[i].transmitter || dataHeader->getAddress3() != states[i].address3 || dataHeader->getAddress4() != states[i].address4 || dataHeader->getType() != states[i].type || dataHeader->getToDS() != states[i].toDS || dataHeader->getFromDS() != states[i].fromDS || (*subframes)[i]->getDataLength() != states[i].dataLength || dataHeader->getChunkLength() != states[i].headerLength || dataTrailer->getChunkLength() != states[i].trailerLength) + throw cRuntimeError("A-MSDU provider changed aggregation-critical frame fields during extraction"); + } + for (auto& subframe : extractedSubframes) + subframe.release(); + auto aggregatedFrame = aMsduAggregation->aggregateFrames(subframes.get()); emit(packetAggregatedSignal, aggregatedFrame); - delete subframes; return aggregatedFrame; } return nullptr; @@ -72,27 +111,49 @@ std::vector *OriginatorQosMacDataService::fragmentIfNeeded(Packet *fra auto fragmentSizes = fragmentationPolicy->computeFragmentSizes(frame); if (fragmentSizes.size() != 0) { emit(packetFragmentedSignal, frame); + auto transactionTag = frame->findTag(); + bool hasTransactionTag = transactionTag != nullptr; + auto transactionId = hasTransactionTag ? transactionTag->getTransactionId() : 0; auto fragmentFrames = fragmentation->fragmentFrame(frame, fragmentSizes); + if (hasTransactionTag) + for (auto fragment : *fragmentFrames) + fragment->addTagIfAbsent()->setTransactionId(transactionId); return fragmentFrames; } return nullptr; } +bool OriginatorQosMacDataService::isFrameEligible(const Packet *packet) const +{ + return !frameEligibilityFunction || frameEligibilityFunction(packet); +} + +bool OriginatorQosMacDataService::hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const +{ + return pendingQueue->findPacket([this](const Packet *packet) { return isFrameEligible(packet); }) != nullptr; +} + std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) { Enter_Method("extractFramesToTransmit"); - if (pendingQueue->isEmpty()) + auto predicate = [this](const Packet *packet) { return isFrameEligible(packet); }; + auto candidate = pendingQueue->findPacket(predicate); + if (candidate == nullptr) return nullptr; else { // if (msduRateLimiting) // txRateLimitingIfNeeded(); Packet *packet = nullptr; + // Scheduling selects the anchor; the policy may select additional + // eligible members, which are all extracted through the provider. if (aMsduAggregationPolicy) - packet = aMsduAggregateIfNeeded(pendingQueue); + packet = aMsduAggregateIfNeeded(pendingQueue, candidate); if (!packet) { - packet = pendingQueue->dequeuePacket(); + packet = pendingQueue->dequeuePacket(predicate); + ASSERT(packet == candidate); take(packet); } + ASSERT(packet != nullptr); // PS Defer Queueing if (sequenceNumberAssignment) { auto header = packet->removeAtFront(); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 0d162c213be..444b006a231 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -38,18 +38,22 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p // PsDeferQueueing *psDeferQueueing = nullptr; IMpduAggregationPolicy *aMpduAggregationPolicy = nullptr; IMpduAggregation *aMpduAggregation = nullptr; + FrameEligibilityFunction frameEligibilityFunction; protected: virtual void initialize() override; virtual void assignSequenceNumber(const Ptr& header); virtual std::vector *fragmentIfNeeded(Packet *frame); - virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue); + virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *candidate); virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); public: virtual ~OriginatorQosMacDataService(); + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) override { this->frameEligibilityFunction = frameEligibilityFunction; } + virtual bool isFrameEligible(const Packet *packet) const override; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const override; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index c1bb688f218..83cf34ebe66 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -49,7 +49,7 @@ void InProgressFrames::forEachChild(cVisitor *v) bool InProgressFrames::hasEligibleFrameToTransmit() { for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront())) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront())) return true; } return false; @@ -82,7 +82,7 @@ Packet *InProgressFrames::getFrameToTransmit() { ensureHasFrameToTransmit(); for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront())) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront())) return frame; } return nullptr; @@ -95,7 +95,7 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) return frameToTransmit; else { for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront()) && frameToTransmit != frame) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront()) && frameToTransmit != frame) return frame; } auto frames = dataService->extractFramesToTransmit(pendingQueue); @@ -120,12 +120,28 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) void InProgressFrames::dropFrame(Packet *packet) { + auto it = std::find(inProgressFrames.begin(), inProgressFrames.end(), packet); + if (it == inProgressFrames.end()) + return; EV_DEBUG << "Dropping frame " << packet->getName() << ".\n"; - inProgressFrames.erase(std::remove(inProgressFrames.begin(), inProgressFrames.end(), packet), inProgressFrames.end()); + inProgressFrames.erase(it); droppedFrames.push_back(packet); emit(packetDequeuedSignal, packet); } +Packet *InProgressFrames::extractFrame(Packet *packet) +{ + Enter_Method("extractFrame"); + auto it = std::find(inProgressFrames.begin(), inProgressFrames.end(), packet); + if (it == inProgressFrames.end()) + return nullptr; + EV_DEBUG << "Extracting frame " << packet->getName() << ".\n"; + inProgressFrames.erase(it); + emit(packetDequeuedSignal, packet); + drop(packet); + return packet; +} + void InProgressFrames::dropFrames(std::set>> seqAndFragNums) { for (auto it = inProgressFrames.begin(); it != inProgressFrames.end();) { diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 91c4a10cca8..35873e928bf 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -47,8 +47,10 @@ class INET_API InProgressFrames : public SimpleModule virtual Packet *getFrameToTransmit(); virtual Packet *getPendingFrameFor(Packet *frame); virtual void dropFrame(Packet *packet); + virtual Packet *extractFrame(Packet *packet); virtual void dropFrames(std::set>> seqAndFragNums); + virtual bool hasEligibleInProgressFrames() { return hasEligibleFrameToTransmit(); } virtual bool hasInProgressFrames() { ensureHasFrameToTransmit(); return hasEligibleFrameToTransmit(); } virtual std::vector getOutstandingFrames(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg index 4725c5e1110..98d1e8dbc75 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg @@ -61,6 +61,7 @@ enum Ieee80211ReasonCode RC_NONASS_CLASS3 = 7; RC_DIASS_MS_LEAVING = 8; RC_NONAUTH_ASS_REQUEST = 9; + RC_END_BA = 37; RC_MESH_PEERING_CANCELLED = 52; RC_MESH_MAX_PEERS = 53; RC_MESH_CONFIGURATION_POLICY_VIOLATION = 54; diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 7e75b7105f6..96772f046b9 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -6,8 +6,8 @@ /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping1 -r 0, 1000s, 8668-d4e7/tplx;e2b1-205b/~tNl;4083-075c/~tND;e1f1-f93c/tyf, PASS, wireless adhoc Ipv4 # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 1486-cbae/tplx;521b-977a/~tNl;f7ee-50fd/~tND;e391-4c6d/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, cca6-4388/tplx;d036-f044/~tNl;a0e3-f6ff/~tND;507d-21cb/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 1486-cbae/tplx;521b-977a/~tNl;f7ee-50fd/~tND;d6e9-44b3/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, bbb9-ef75/tplx;b738-d1b7/~tNl;47b1-07f1/~tND;c7de-0741/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 9c66-e6f0/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 85bd-85c6/tplx;22f6-085d/~tNl;e4ea-962c/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -655,10 +655,10 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;7a95-cf35/~tND, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, c3c0-dc93/tplx;5eb9-6cea/~tNl;87a5-11a1/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 3a58-19c0/tplx;6188-6ffd/~tNl;377c-d4fc/~tND;100e-198d/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 3a58-19c0/tplx;6188-6ffd/~tNl;377c-d4fc/~tND;7647-5f80/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 33e5-c380/tplx;78d2-bb9a/~tNl;1524-d88f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 7f3b-72ca/tplx;7c57-849b/~tNl;06e3-7901/~tND;2fc2-2ac9/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 6f59-bd7d/tplx;c3ce-3389/~tNl;9e1e-7c17/~tND;6880-4ff1/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 7f3b-72ca/tplx;7c57-849b/~tNl;06e3-7901/~tND;3d84-93de/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 5570-9d59/tplx;c521-a55d/~tNl;e5fc-a44c/~tND;e59c-a8ed/tyf, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index e72c62f8d96..d611f9ff408 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -192,16 +192,16 @@ /showcases/visualizer/canvas/transportpathactivity/, -f omnetpp.ini -c Filtering -r 0, 100s, 8bcf-d426/tplx;8f09-7a80/~tNl;92a4-c3ec/~tND;0694-d5be/tyf, PASS, wireless EthernetMac Ipv4 /showcases/wireless/aggregation/, -f omnetpp.ini -c NoAggregation -r 0, 1s, 97f6-a969/tplx;ecf2-6e33/~tNl;5d9e-8fdf/~tND;27f2-4a52/tyf, PASS, wireless Ipv4 -/showcases/wireless/aggregation/, -f omnetpp.ini -c Aggregation -r 0, 1s, 671b-274c/tplx;6d84-9720/~tNl;b29d-4083/~tND;95cc-1859/tyf, PASS, wireless Ipv4 -/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/aggregation/, -f omnetpp.ini -c Aggregation -r 0, 1s, 671b-274c/tplx;6d84-9720/~tNl;b29d-4083/~tND;4292-f684/tyf, PASS, wireless Ipv4 +/showcases/wireless/aggregation/, -f omnetpp.ini -c VoicePriorityAggregation -r 0, 1s, 1dbe-6672/tplx;a253-dbe3/~tNl;3bb2-9797/~tND;f028-4b38/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 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 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 7f81-62c8/tplx;2638-6c00/~tNl;9837-a9de/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 26b2-73e4/tplx;d7bc-6c9e/~tNl;d59c-1322/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, e9fb-242d/tplx;6719-4a98/~tNl;1fc9-fb0d/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 74c6-3e8e/tplx;7254-8cce/~tNl;1859-74f1/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 3f8b-f7c7/tplx;5d7e-2fb7/~tNl;1ff2-3b26/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;fc60-7eb7/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;6f3a-074a/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -267,7 +267,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 57ee-7ddf/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, f7a5-cf0a/tplx;a24a-a070/~tNl;5dae-0b8e/~tND;11a9-72e7/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 5abb-93a6/tplx;2bdd-d1fa/~tNl;31ab-28b7/~tND;a065-ca9a/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 892b-8baa/tplx;370a-d694/~tNl;ab8b-0a8a/~tND;641e-b548/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 3623-0ff6/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -314,7 +314,7 @@ /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/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 26be-ff05/tplx;9c39-00a3/~tNl;03a9-2f02/~tND;5fef-ddb3/tyf, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 8d7c-5091/tplx;84a6-b893/~tNl;cb18-5b4d/~tND;6ba7-4c7c/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 8d7c-5091/tplx;84a6-b893/~tNl;cb18-5b4d/~tND;c7c6-29c8/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 @@ -341,5 +341,5 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb5-d354/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, eb27-b910/tplx;7c61-294a/~tNl;29d5-c1ad/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, 9032-d5ab/tplx;6276-d05a/~tNl;7526-602e/tyf, PASS, wireless Ipv4 diff --git a/tests/module/Ieee80211HcfAddbaRedDrop_1.test b/tests/module/Ieee80211HcfAddbaRedDrop_1.test new file mode 100644 index 00000000000..005359b36b0 --- /dev/null +++ b/tests/module/Ieee80211HcfAddbaRedDrop_1.test @@ -0,0 +1,176 @@ +%description: +Exercise real HCF terminal cleanup when bounded management queues drop expiry +DELBAs for both roles and when RED rejects a pending ADDBA before enqueue. + +%file: TestHcfAgreementQueueDrop.cc +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + +namespace inet { +namespace ieee80211 { + +class SeedOriginatorHandler : public OriginatorBlockAckAgreementHandler +{ + public: + Packet *request(MacAddress peer, Tid tid, IOriginatorBlockAckAgreementPolicy *policy) { + auto header = buildAddbaRequest(peer, tid, SequenceNumberCyclic(1), policy); + auto id = nextTransactionId++; + createAgreement(header, id); + auto packet = new Packet("ADDBA", header); + packet->addTag()->setTransactionId(id); + return packet; + } +}; + +class QueueDropHcf : public Hcf +{ + public: + void prepare(MacAddress peer) { + Enter_Method("prepare"); + ASSERT(originatorBlockAckAgreementHandler != nullptr); + delete originatorBlockAckAgreementHandler; + originatorBlockAckAgreementHandler = new SeedOriginatorHandler(); + } + void rejectRequest(MacAddress peer) { + Enter_Method("rejectRequest"); + auto handler = check_and_cast(originatorBlockAckAgreementHandler); + auto packet = handler->request(peer, 0, originatorBlockAckAgreementPolicy); + auto header = packet->peekAtFront(); + auto staleCopy = packet->dup(); + ASSERT(handler->isAddbaResponsePending(peer, 0)); + auto dataHeader = makeShared(); + dataHeader->setType(ST_DATA_WITH_QOS); + dataHeader->setReceiverAddress(peer); + dataHeader->setTid(0); + auto data = new Packet("eligibility", dataHeader); + ASSERT(!originatorDataService->isFrameEligible(data)); + processMgmtFrame(packet, header); + ASSERT(handler->getAgreement(peer, 0) == nullptr); + ASSERT(!handler->isAddbaRequestPending(staleCopy, header)); + ASSERT(!addbaResponseTimer->isScheduled()); + ASSERT(originatorDataService->isFrameEligible(data)); + for (const auto& entry : pendingFrameEligibility) + ASSERT(entry.first->findTag() == nullptr); + delete data; + delete staleCopy; + } + +}; +Define_Module(QueueDropHcf); + +class HcfAgreementQueueDropTest : public cSimpleModule, public cListener +{ + public: + HcfAgreementQueueDropTest() : cSimpleModule(65536) {} + protected: + int departures = 0; + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *, cObject *details) override { + if (std::string(source->getName()) == "pendingQueue") { + ASSERT(check_and_cast(details)->getReason() == queueing::IPacketQueue::PacketRemovalReason::DROPPED); + departures++; + } + } + virtual void activity() override { + const MacAddress peer("02:00:00:00:00:09"); + auto hcf = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf")); + auto queueModule = getModuleByPath("^.ap.wlan[0].mac.hcf.edca.edcaf[3].pendingQueue"); + queueModule->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + hcf->prepare(peer); + // Fill RED's FIFO without allowing EDCA to drain it. + auto queue = check_and_cast(queueModule->getSubmodule("managementQueue")); + auto header = makeShared(); + header->setReceiverAddress(peer); + queue->enqueuePacket(new Packet("occupant", header)); + hcf->rejectRequest(peer); + ASSERT(departures == 1); + queueModule->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + wait(SimTime(1, SIMTIME_US)); + std::cout << "HCF agreement queue cleanup verified.\n"; + } +}; +Define_Module(HcfAgreementQueueDropTest); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.queueing.classifier.PacketClassifier; +import inet.queueing.queue.CompoundPacketQueueBase; +import inet.queueing.queue.PacketQueue; +import inet.queueing.queue.RedDropperQueue; +import inet.queueing.scheduler.PriorityScheduler; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module QueueDropHcf extends Hcf { + parameters: + bool redTest = default(false); + @class(::inet::ieee80211::QueueDropHcf); +} +module AgreementRedQueue extends RedDropperQueue { + parameters: + @class(::inet::queueing::CompoundPacketQueueBase); + red.minth = 0; + red.maxth = 1; + red.packetCapacity = 1; + red.wq = 1; + red.maxp = 0; +} +module AgreementPendingQueue extends CompoundPacketQueueBase { + parameters: + @class(::inet::queueing::CompoundPacketQueueBase); + submodules: + classifier: PacketClassifier { classifierClass = "inet::ieee80211::MgmtOverDataClassifier"; } + managementQueue: like inet.queueing.contract.IPacketQueue; + multicastQueue: PacketQueue; + unicastQueue: PacketQueue; + prioritizer: PriorityScheduler; + connections: + in --> classifier.in; + classifier.out++ --> managementQueue.in; + classifier.out++ --> multicastQueue.in; + classifier.out++ --> unicastQueue.in; + managementQueue.out --> prioritizer.in++; + multicastQueue.out --> prioritizer.in++; + unicastQueue.out --> prioritizer.in++; + prioritizer.out --> out; +} +simple HcfAgreementQueueDropTest extends SimpleModule { + parameters: + @class(::inet::ieee80211::HcfAgreementQueueDropTest); +} +network HcfAgreementQueueDropNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + test: HcfAgreementQueueDropTest; +} + +%inifile: omnetpp.ini +[General] +network = HcfAgreementQueueDropNetwork +sim-time-limit = 4us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.ap.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.ap.wlan[0].mac.qosStation = true +*.ap.wlan[0].mac.hcf.isBlockAckSupported = true +*.ap.wlan[0].mac.hcf.redTest = true +*.ap.wlan[0].mac.hcf.edca.edcaf[*].pendingQueue.managementQueue.typename = "AgreementRedQueue" +*.ap.wlan[0].mac.hcf.typename = "QueueDropHcf" +*.ap.wlan[0].mac.hcf.edca.edcaf[*].pendingQueue.typename = "AgreementPendingQueue" +*.ap.wlan[0].mgmt.beaconInterval = 10s +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.ap.wlan[0].address = "02:00:00:00:00:01" + +%contains: stdout +HCF agreement queue cleanup verified. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 91a9667ac13..ea5449df999 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -11,6 +11,23 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/common/TimeTag_m.h" #include "inet/common/LabelsTag_m.h" #include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/queueing/gate/PacketGate.h" #include "inet/queueing/gate/PeriodicGate.h" @@ -23,6 +40,100 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC %global: using namespace inet; using namespace inet::ieee80211; + +class TestOriginatorPolicy : public IOriginatorBlockAckAgreementPolicy +{ + public: + simtime_t addbaResponseTimeout = 1; + simtime_t addbaRetryBackoff = 2; + bool addbaAccepted = true; + bool delbaAccepted = true; + + virtual bool isAddbaReqNeeded(Packet *, const Ptr&) override { return true; } + virtual bool isAddbaReqAccepted(const Ptr&, OriginatorBlockAckAgreement *) override { return addbaAccepted; } + virtual bool isDelbaAccepted(const Ptr&) override { return delbaAccepted; } + virtual bool isMsduSupported() const override { return true; } + virtual simtime_t getAddbaResponseTimeout() const override { return addbaResponseTimeout; } + virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } + virtual simtime_t getBlockAckTimeoutValue() const override { return 0; } + virtual bool isDelayedAckPolicySupported() const override { return false; } + virtual int getMaximumAllowedBufferSize() const override { return 64; } +}; + +class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy +{ + public: + bool accepted = true; + bool delbaAccepted = true; + simtime_t blockAckTimeoutValue = 0; + bool aMsduSupportedValue = true; + bool delayedBlockAckPolicySupportedValue = false; + int maximumAllowedBufferSize = 64; + + virtual bool isAddbaReqAccepted(const Ptr&) override { return accepted; } + virtual bool isDelbaAccepted(const Ptr&) override { return delbaAccepted; } + virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } + virtual bool aMsduSupported() const override { return aMsduSupportedValue; } + virtual bool delayedBlockAckPolicySupported() const override { return delayedBlockAckPolicySupportedValue; } + virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } +}; + +class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback +{ + public: + Packet *managementPacket = nullptr; + std::vector managementPackets; + Ptr managementHeader; + simtime_t addbaDeadline = SIMTIME_MAX; + std::vector cancelledTransactionIds; + std::vector excludedPackets; + + virtual ~TestCallback() { for (auto packet : managementPackets) delete packet; } + void forgetManagementPacket(Packet *packet) { managementPackets.erase(std::remove(managementPackets.begin(), managementPackets.end(), packet), managementPackets.end()); } + virtual void transmitControlResponseFrame(Packet *, const Ptr&, Packet *, const Ptr&) override { throw cRuntimeError("Unexpected control response"); } + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementPacket = packet; + managementPackets.push_back(packet); + managementHeader = header; + } + virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleAddbaResponseTimer(simtime_t deadline) override { addbaDeadline = deadline; } + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { + cancelledTransactionIds.push_back(transactionId); + excludedPackets.push_back(excludedPacket); + } +}; + +class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler +{ + public: + using OriginatorBlockAckAgreementHandler::buildAddbaRequest; + void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, 1); + agreement->setIsAddbaResponseReceived(true); + blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; + } + void setNextDialogToken(uint8_t token) { nextDialogToken = token; } + void expireNow(MacAddress receiverAddress, Tid tid) { getAgreement(receiverAddress, tid)->setAddbaResponseDeadline(simTime()); } + void allowRetryNow(MacAddress receiverAddress, Tid tid) { addbaRetryDeadlines[std::make_pair(receiverAddress, tid)] = simTime(); } + void setPendingTeardownTransaction(MacAddress receiverAddress, Tid tid, uint64_t transactionId) { pendingTeardownTransactionIds[std::make_pair(receiverAddress, tid)] = transactionId; } + simtime_t getRetryDeadline(MacAddress receiverAddress, Tid tid) const { auto it = addbaRetryDeadlines.find(std::make_pair(receiverAddress, tid)); return it == addbaRetryDeadlines.end() ? SIMTIME_MAX : it->second; } +}; + +class TestTimeoutCancellationCallback : public TestCallback +{ + public: + TestOriginatorHandler *handler = nullptr; + std::vector> expiredAgreementIds; + bool allExpiredAgreementsGoneBeforeCallbacks = true; + + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { + for (const auto& agreementId : expiredAgreementIds) + allExpiredAgreementsGoneBeforeCallbacks &= handler->getAgreement(agreementId.first, agreementId.second) == nullptr; + TestCallback::cancelAddbaTransaction(transactionId, excludedPacket); + } +}; + class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue { public: @@ -92,6 +203,37 @@ class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue virtual void pushPacketEnd(Packet *, const cGate *) override { throw cRuntimeError("Unsupported"); } virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } }; + +class TestWrongDequeueQueue : public TestPacketQueue +{ + public: + Packet *wrongPacket = nullptr; + int numPredicateDequeues = 0; + + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + if (numPredicateDequeues++ == 0) + return TestPacketQueue::dequeuePacket(predicate); + packets.erase(std::find(packets.begin(), packets.end(), wrongPacket)); + notifyPacketRemoved(wrongPacket, PacketRemovalReason::DEQUEUED); + return wrongPacket; + } +}; + +class TestRepeatedDequeueQueue : public TestPacketQueue +{ + public: + Packet *repeatedPacket = nullptr; + int numPredicateDequeues = 0; + + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + if (numPredicateDequeues++ == 0) { + repeatedPacket = TestPacketQueue::dequeuePacket(predicate); + return repeatedPacket; + } + return repeatedPacket; + } +}; + class TestCompoundPacketQueue : public queueing::CompoundPacketQueueBase { public: @@ -249,6 +391,261 @@ class TestPacketArrivalListener : public cListener lastArrivalGate = check_and_cast(value)->getArrivalGate(); } }; + +class TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + +class TestPacketDropSignalListener : public cListener +{ + public: + int numSignals = 0; + int numOtherPacketDrops = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *details) override { + numSignals++; + auto packetDropDetails = check_and_cast(details); + if (packetDropDetails->getReason() == OTHER_PACKET_DROP) + numOtherPacketDrops++; + } +}; + +class TestAckHandler : public IAckHandler +{ + public: + virtual bool isEligibleToTransmit(const Ptr&) override { return true; } + virtual bool isOutstandingFrame(const Ptr&) override { return false; } + virtual void frameGotInProgress(const Ptr&) override {} +}; + +class TestQosAckHandler : public QosAckHandler +{ + public: + bool hasMgmtEntry(const Ptr& header) const { + auto id = std::make_pair(header->getReceiverAddress(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + return mgmtAckStatuses.find(id) != mgmtAckStatuses.end(); + } +}; + +class TestInProgressFrames : public InProgressFrames +{ + public: + void configure(IOriginatorMacDataService *dataService, IAckHandler *ackHandler, queueing::IPacketQueue *pendingQueue = nullptr) { + this->dataService = dataService; + this->ackHandler = ackHandler; + this->pendingQueue = pendingQueue; + } + void addFrame(Packet *packet) { inProgressFrames.push_back(packet); } + void addOwnedFrame(Packet *packet) { take(packet); inProgressFrames.push_back(packet); } + bool hasEligibleFrame() { return hasEligibleFrameToTransmit(); } + int getNumDroppedFrames() const { return droppedFrames.size(); } + std::vector releaseFrames() { + std::vector frames = inProgressFrames; + frames.insert(frames.end(), droppedFrames.begin(), droppedFrames.end()); + for (auto frame : frames) + if (frame->getOwner() == this) + drop(frame); + inProgressFrames.clear(); + droppedFrames.clear(); + return frames; + } +}; + +class TestEdcaf : public Edcaf +{ + public: + queueing::IPacketQueue *pendingQueue = nullptr; + InProgressFrames *inProgressFrames = nullptr; + QosAckHandler *qosAckHandler = nullptr; + QosRecoveryProcedure *qosRecoveryProcedure = nullptr; + StationRetryCounters stationRetryCounters; + int numReleaseChannelCalls = 0; + std::vector *eventOrder = nullptr; + bool contentionActive = false; + int numRequestChannelCalls = 0; + int numContentionStarts = 0; + + virtual queueing::IPacketQueue *getPendingQueue() const override { return pendingQueue; } + virtual InProgressFrames *getInProgressFrames() const override { return inProgressFrames; } + virtual QosAckHandler *getAckHandler() const override { return qosAckHandler; } + virtual QosRecoveryProcedure *getRecoveryProcedure() const override { return qosRecoveryProcedure; } + virtual StationRetryCounters *getStationRetryCounters() const override { return const_cast(&stationRetryCounters); } + virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleaseChannelCalls++; if (eventOrder != nullptr) eventOrder->push_back(1); } + virtual void requestChannel(IChannelAccess::ICallback *) override { numRequestChannelCalls++; if (!contentionActive) numContentionStarts++; } + void setAccessCategory(AccessCategory accessCategory) { ac = accessCategory; } + void setTxopProcedure(TxopProcedure *txopProcedure) { this->txopProcedure = txopProcedure; } +}; + +class TestEdca : public Edca +{ + public: + Edcaf *edcaf = nullptr; + std::array edcafs = {}; + int numEdcafs = 1; + NonQosRecoveryProcedure *nonQosRecoveryProcedure = nullptr; + std::vector requestedAccessCategories; + std::vector internallyCollidedEdcafs; + std::vector *eventOrder = nullptr; + + virtual int getNumEdcafs() const override { return numEdcafs; } + virtual Edcaf *getEdcaf(AccessCategory accessCategory) const override { return numEdcafs == 1 ? edcaf : edcafs[accessCategory]; } + virtual Edcaf *getChannelOwner() override { return edcaf; } + virtual std::vector getInternallyCollidedEdcafs() override { return internallyCollidedEdcafs; } + virtual void requestChannelAccess(AccessCategory accessCategory, IChannelAccess::ICallback *callback) override { requestedAccessCategories.push_back(accessCategory); if (eventOrder != nullptr) eventOrder->push_back(3); getEdcaf(accessCategory)->requestChannel(callback); } + virtual NonQosRecoveryProcedure *getMgmtAndNonQoSRecoveryProcedure() const override { return nonQosRecoveryProcedure; } +}; + +class TestHcf : public Hcf +{ + public: + virtual ~TestHcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + int numProcessedDroppedSetupFrames = 0; + int numResumedEligibleChannelAccess = 0; + int numCancelledAddbaTransactions = 0; + int numRebuildEligibilityCalls = 0; + bool droppedSetupCancelled = false; + bool delegateDroppedSetupHandling = false; + TestCallback *managementCallback = nullptr; + + void configure(Edca *edca) { this->edca = edca; } + void configureMac(Ieee80211Mac *mac) { this->mac = mac; subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); } + void configureEligibilityIndex(Edca *edca, IOriginatorMacDataService *originatorDataService) { + this->edca = edca; + this->originatorDataService = originatorDataService; + } + void rebuildEligibilityIndex() { rebuildPendingFrameEligibility(); } + void trackFrame(Packet *packet, AccessCategory accessCategory) { trackPendingFrame(packet, accessCategory); } + void untrackFrame(Packet *packet) { untrackPendingFrame(packet); } + bool hasFrame(AccessCategory accessCategory) { return hasFrameToTransmit(accessCategory); } + void requestEligible() { requestEligibleChannelAccess(); } + int processInternalCollision(const std::vector& edcafs) { return handleInternalCollision(edcafs); } + void grantChannel(IChannelAccess *channelAccess) { channelGranted(channelAccess); } + void finishSequence() { frameSequenceFinished(); } + int getNumEligiblePendingFrames(AccessCategory accessCategory) const { return numEligiblePendingFrames[accessCategory]; } + int getNumTrackedPendingFrames() const { return pendingFrameEligibility.size(); } + void observePendingQueue(queueing::IPacketQueue *pendingQueue) { + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + } + void configureFrameSequenceHandler(IFrameSequenceHandler *frameSequenceHandler) { this->frameSequenceHandler = frameSequenceHandler; } + void configureRecipientDataService(IRecipientQosMacDataService *recipientDataService) { this->recipientDataService = recipientDataService; } + void configureManagementCallback(TestCallback *managementCallback) { this->managementCallback = managementCallback; } + void configureTransmittedData(Edca *edca, IOriginatorBlockAckAgreementHandler *handler, IOriginatorBlockAckAgreementPolicy *policy) { + this->edca = edca; + originatorBlockAckAgreementHandler = handler; + originatorBlockAckAgreementPolicy = policy; + } + void configureTransmittedManagement(Edca *edca, IOriginatorQoSAckPolicy *originatorAckPolicy) { + this->edca = edca; + this->originatorAckPolicy = originatorAckPolicy; + } + void configureBlockAckHandlers(IOriginatorBlockAckAgreementHandler *originatorHandler, IOriginatorBlockAckAgreementPolicy *originatorPolicy, IRecipientBlockAckAgreementHandler *recipientHandler, IRecipientBlockAckAgreementPolicy *recipientPolicy) { + originatorBlockAckAgreementHandler = originatorHandler; + originatorBlockAckAgreementPolicy = originatorPolicy; + recipientBlockAckAgreementHandler = recipientHandler; + recipientBlockAckAgreementPolicy = recipientPolicy; + } + void processTransmittedData(Packet *packet, const Ptr& dataHeader, AccessCategory ac) { originatorProcessTransmittedDataFrame(packet, dataHeader, ac); } + void processTransmittedManagement(Packet *packet, const Ptr& managementHeader, AccessCategory ac) { originatorProcessTransmittedManagementFrame(packet, managementHeader, ac); } + void dropPacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DROPPED); } + void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } + void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } + void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } + void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } + void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { + auto header = packet->peekAtFront(); + auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); + originatorProcessReceivedControlFrame(packet, header, lastTransmittedPacket, lastTransmittedHeader, accessCategory); + } + + protected: + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (managementCallback != nullptr) + managementCallback->processMgmtFrame(packet, header); + else + Hcf::processMgmtFrame(packet, header); + } + virtual bool processDroppedBlockAckSetupFrame(Packet *packet) override { numProcessedDroppedSetupFrames++; return delegateDroppedSetupHandling ? Hcf::processDroppedBlockAckSetupFrame(packet) : droppedSetupCancelled; } + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { numCancelledAddbaTransactions++; Hcf::cancelAddbaTransaction(transactionId, excludedPacket); } + virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } + virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } + virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleAddbaResponseTimer(simtime_t) override {} +}; + +class TestMac : public Ieee80211Mac +{ + public: + using Ieee80211Mac::receiveSignal; + int numPendingRadioConfigSends = 0; + virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } +}; + +class TestFragmentationPolicy : public IFragmentationPolicy +{ + public: + virtual std::vector computeFragmentSizes(Packet *) override { return { 2, 2 }; } +}; + +class TestMsduAggregationPolicy : public IMsduAggregationPolicy +{ + public: + int numCalls = 0; + + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *, Packet *, const std::function&) override { numCalls++; return nullptr; } +}; + +class TestSelectedMsduAggregationPolicy : public IMsduAggregationPolicy +{ + public: + int numCalls = 0; + std::vector selectedPackets; + + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *, Packet *, const std::function&) override { numCalls++; return new std::vector(selectedPackets); } +}; + +class TestBasicMsduAggregationPolicy : public BasicMsduAggregationPolicy +{ + public: + void configure(b maximumSize) { + qOsCheck = true; + subframeNumThreshold = -1; + aggregationLengthThreshold = -1; + maxAMsduSize = maximumSize; + } +}; + +class TestNonQosRecoveryProcedure : public NonQosRecoveryProcedure +{ + public: + void addRetry(const Ptr& header) { + rtsThreshold = INT_MAX; + shortRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = 1; + } + void addShortRetry(const Ptr& header, int count = 1) { + shortRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = count; + } + void addLongRetry(const Ptr& header, int count = 1) { + longRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = count; + } + bool hasRetry(const Ptr& header) const { + return shortRetryCounter.find(SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())) != shortRetryCounter.end(); + } + void configureRts(int threshold, int retryLimit, ICwCalculator *cwCalculator) { + rtsThreshold = threshold; + shortRetryLimit = retryLimit; + longRetryLimit = retryLimit; + this->cwCalculator = cwCalculator; + } +}; + class TestOnlyPacketExtractor : public queueing::IPacketExtractor { public: @@ -265,6 +662,59 @@ class TestOnlyPacketExtractor : public queueing::IPacketExtractor return packet; } }; + +class TestCwCalculator : public IRecoveryProcedure::ICwCalculator +{ + public: + int cw = 1; + virtual void incrementCw() override { cw++; } + virtual void resetCw() override { cw = 1; } + virtual int getCw() override { return cw; } +}; + +class TestOriginatorQosMacDataService : public OriginatorQosMacDataService +{ + public: + void enableMsduAggregationPolicy(IMsduAggregationPolicy *policy) { + aMsduAggregationPolicy = policy; + if (aMsduAggregation == nullptr) + aMsduAggregation = new MsduAggregation(); + } + void enableSequenceNumberAssignment() { sequenceNumberAssignment = new QoSSequenceNumberAssignment(); } + void enableFragmentation(IFragmentationPolicy *policy) { + fragmentationPolicy = policy; + fragmentation = new Fragmentation(); + } + std::vector *fragment(Packet *packet) { return fragmentIfNeeded(packet); } + void release(Packet *packet) { drop(packet); } +}; + +class TestFrameSequenceHandler : public IFrameSequenceHandler +{ + public: + IFrameSequence *frameSequence = nullptr; + FrameSequenceContext *context = nullptr; + bool running = false; + int numStartedSequences = 0; + + virtual ~TestFrameSequenceHandler() { + delete frameSequence; + delete context; + } + virtual const FrameSequenceContext *getContext() const override { return context; } + virtual const IFrameSequence *getFrameSequence() const override { return frameSequence; } + virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, ICallback *) override { + this->frameSequence = frameSequence; + this->context = context; + running = true; + numStartedSequences++; + } + virtual void processResponse(Packet *) override {} + virtual void transmissionComplete() override {} + virtual bool isSequenceRunning() override { return running; } + virtual void handleStartRxTimeout() override {} +}; + static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) { auto header = makeShared(); @@ -274,8 +724,41 @@ static Ptr makeQosHeader(MacAddress receiverAddress, Tid ti header->setSequenceNumber(sequenceNumber); return header; } + +static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t transactionId) +{ + auto delba = makeShared(); + delba->setReceiverAddress(receiverAddress); + delba->setTid(tid); + delba->setInitiator(true); + delba->setSequenceNumber(sequenceNumber); + delba->setFragmentNumber(fragmentNumber); + delba->setMoreFragments(moreFragments); + auto packet = new Packet(name, delba); + packet->addTag()->setTransactionId(transactionId); + return packet; +} + +static Ptr makeResponse(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, uint16_t statusCode) +{ + auto response = makeShared(); + response->setTransmitterAddress(transmitterAddress); + response->setTid(tid); + response->setDialogToken(dialogToken); + response->setStatusCode(statusCode); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(0); + return response; +} + + %activity: +// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.2. +TestOriginatorPolicy originatorPolicy; +TestCallback callback; MacAddress peer1("00:00:00:00:00:01"); +MacAddress peer2("00:00:00:00:00:02"); +MacAddress peer3("00:00:00:00:00:03"); auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const char *dropperClass = "") { auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); module->par("packetCapacity").setIntValue(packetCapacity); @@ -283,6 +766,749 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const module->callInitialize(); return check_and_cast(module); }; + +{ + TestOriginatorHandler tokenHandler; + tokenHandler.setNextDialogToken(255); + auto request255 = tokenHandler.buildAddbaRequest(peer1, 0, SequenceNumberCyclic(0), &originatorPolicy); + auto request1 = tokenHandler.buildAddbaRequest(peer1, 0, SequenceNumberCyclic(0), &originatorPolicy); + ASSERT(request255->getDialogToken() == 255); + ASSERT(request1->getDialogToken() == 1); +} + +TestOriginatorHandler handler; +Packet triggerPacket("trigger"); +auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); + +// Transmitting the trigger MPDU does not create an agreement; initiation is +// deferred until the HCF successful-ACK path calls processAcknowledgedDataFrame(). +{ + auto preAckHandler = new TestOriginatorHandler(); + QosAckHandler preAckAckHandler; + TestEdcaf preAckEdcaf; + preAckEdcaf.qosAckHandler = &preAckAckHandler; + TestEdca preAckEdca; + preAckEdca.edcaf = &preAckEdcaf; + TestHcf preAckHcf; + preAckHcf.configureTransmittedData(&preAckEdca, preAckHandler, &originatorPolicy); + Packet preAckPacket("preAckPacket"); + auto preAckHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); + preAckHeader->setAckPolicy(NORMAL_ACK); + preAckHcf.processTransmittedData(&preAckPacket, preAckHeader, AC_BE); + if (preAckHandler->getAgreement(peer1, 3) != nullptr) + throw cRuntimeError("HCF initiated ADDBA before the trigger MPDU was acknowledged"); + ASSERT(preAckHandler->getAgreement(peer1, 3) == nullptr); +} + +// A fragmented MSDU cannot trigger ADDBA until its final fragment is ACKed, +// otherwise the remaining fragments would precede the advertised SSN. +{ + TestOriginatorHandler fragmentedDataHandler; + TestCallback fragmentedDataCallback; + Packet fragmentedDataPacket("fragmentedDataPacket"); + auto firstFragmentHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + firstFragmentHeader->setFragmentNumber(0); + firstFragmentHeader->setMoreFragments(true); + fragmentedDataHandler.processAcknowledgedDataFrame(&fragmentedDataPacket, firstFragmentHeader, &originatorPolicy, &fragmentedDataCallback); + if (fragmentedDataHandler.getAgreement(peer1, 6) != nullptr || fragmentedDataCallback.managementPacket != nullptr) + throw cRuntimeError("ADDBA was initiated before the final data fragment was acknowledged"); + auto finalFragmentHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + finalFragmentHeader->setFragmentNumber(1); + finalFragmentHeader->setMoreFragments(false); + fragmentedDataHandler.processAcknowledgedDataFrame(&fragmentedDataPacket, finalFragmentHeader, &originatorPolicy, &fragmentedDataCallback); + auto fragmentedDataRequest = dynamicPtrCast(fragmentedDataCallback.managementHeader); + ASSERT(fragmentedDataRequest != nullptr); + ASSERT(fragmentedDataRequest->getStartingSequenceNumber() == SequenceNumberCyclic(101)); +} + +// A fragmented, transaction-tagged DELBA remains eligible through every +// nonfinal fragment. Aborting one fragment invalidates the transaction and asks +// the HCF callback to remove its sibling fragments. +{ + TestOriginatorHandler fragmentedDelbaHandler; + TestCallback fragmentedDelbaCallback; + auto firstDelba = makeShared(); + firstDelba->setReceiverAddress(peer1); + firstDelba->setTid(6); + firstDelba->setInitiator(true); + firstDelba->setFragmentNumber(0); + firstDelba->setMoreFragments(true); + auto finalDelba = staticPtrCast(firstDelba->dupShared()); + finalDelba->setFragmentNumber(1); + finalDelba->setMoreFragments(false); + Packet firstDelbaPacket("firstDelbaFragment", firstDelba); + Packet finalDelbaPacket("finalDelbaFragment", finalDelba); + firstDelbaPacket.addTag()->setTransactionId(77); + finalDelbaPacket.addTag()->setTransactionId(77); + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 77); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&firstDelbaPacket, &fragmentedDelbaCallback) == nullptr); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&finalDelbaPacket, &fragmentedDelbaCallback) == nullptr); + // A failed attempt remains eligible for retransmission. + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&finalDelbaPacket, &fragmentedDelbaCallback) == nullptr); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(!fragmentedDelbaHandler.processAcknowledgedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(fragmentedDelbaHandler.processAcknowledgedDelba(&finalDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &finalDelbaPacket); + + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); + firstDelbaPacket.getTagForUpdate()->setTransactionId(78); + ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 78); + ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &firstDelbaPacket); +} + +// IEEE Std 802.11-2024, 10.23.2.12.1 and 11.5.3.2: the HCF retry path keeps a +// transaction-tagged DELBA selectable and preserves its negative ACK state. +{ + constexpr uint64_t transactionId = 79; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto packet = makeTaggedDelbaPacket("failedTransactionalDelba", peer1, 6, SequenceNumberCyclic(203), 0, false, transactionId); + auto delba = packet->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(packet); + ackHandler.frameGotInProgress(delba); + hcf.processTransmittedManagement(packet, delba, AC_BE); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(delba) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + hcf.processFailedFrame(packet); + ASSERT(originatorHandler->isDelbaPending(packet, packet->peekAtFront())); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(delba) == QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED); + ASSERT(ackHandler.hasMgmtEntry(delba)); + ASSERT(inProgressFrames.getFrameToTransmit() == packet); + ASSERT(inProgressFrames.getNumDroppedFrames() == 0); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ packet })); + delete packet; +} + +// A final-fragment ACK retires the DELBA transaction through HCF exactly once, +// removes its in-progress sibling, and clears both ACK-handler entries. +{ + constexpr uint64_t transactionId = 80; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + TestMac acknowledgedDelbaMac; + hcf.configureMac(&acknowledgedDelbaMac); + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto sibling = makeTaggedDelbaPacket("acknowledgedDelbaSibling", peer1, 6, SequenceNumberCyclic(204), 0, true, transactionId); + auto finalPacket = makeTaggedDelbaPacket("acknowledgedFinalDelba", peer1, 6, SequenceNumberCyclic(204), 1, false, transactionId); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(sibling); + inProgressFrames.addOwnedFrame(finalPacket); + ackHandler.frameGotInProgress(siblingHeader); + ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); + Packet ackPacket("transactionalDelbaAck", makeShared()); + hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); + ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); + ASSERT(!originatorHandler->processAcknowledgedDelba(finalPacket, &hcf)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ finalPacket })); + delete finalPacket; +} + +// Reaching the retry limit aborts the teardown through HCF, clears its sibling +// and both ACK-handler entries, and records one terminal cancellation. +{ + constexpr uint64_t transactionId = 81; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + TestMac retryLimitedDelbaMac; + hcf.configureMac(&retryLimitedDelbaMac); + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto sibling = makeTaggedDelbaPacket("retryLimitedDelbaSibling", peer1, 6, SequenceNumberCyclic(205), 0, true, transactionId); + auto finalPacket = makeTaggedDelbaPacket("retryLimitedFinalDelba", peer1, 6, SequenceNumberCyclic(205), 1, false, transactionId); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(sibling); + inProgressFrames.addOwnedFrame(finalPacket); + ackHandler.frameGotInProgress(siblingHeader); + ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); + hcf.processFailedFrame(finalPacket); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); + ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); + ASSERT(!originatorHandler->processAbortedDelba(finalPacket, &hcf)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ finalPacket })); + delete finalPacket; +} + +handler.processAcknowledgedDataFrame(&triggerPacket, triggerHeader, &originatorPolicy, &callback); +auto request = dynamicPtrCast(callback.managementHeader); +ASSERT(request != nullptr); +ASSERT(request->getDialogToken() != 0); +ASSERT(request->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +handler.processTransmittedAddbaReq(callback.managementPacket, request, &originatorPolicy, &callback); +ASSERT(callback.addbaDeadline == simTime() + 1); +auto requestTransactionId = callback.managementPacket->getTag()->getTransactionId(); + +auto wrongToken = makeResponse(peer1, 3, request->getDialogToken() + 1, 0); +ASSERT(handler.processReceivedAddbaResp(wrongToken, &originatorPolicy, &callback).establishedAgreement == nullptr); +ASSERT(handler.isAddbaResponsePending(peer1, 3)); +auto wrongTid = makeResponse(peer1, 4, request->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(wrongTid, &originatorPolicy, &callback).establishedAgreement == nullptr); +auto wrongPeer = makeResponse(peer2, 3, request->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(wrongPeer, &originatorPolicy, &callback).establishedAgreement == nullptr); + +auto success = makeResponse(peer1, 3, request->getDialogToken(), 0); +auto establishedResponse = handler.processReceivedAddbaResp(success, &originatorPolicy, &callback); +auto established = establishedResponse.establishedAgreement; +ASSERT(established != nullptr); +ASSERT(establishedResponse.terminatedAgreement == nullptr); +ASSERT(establishedResponse.teardownDelba == nullptr); +ASSERT(establishedResponse.teardownTransactionId == 0); +ASSERT(callback.cancelledTransactionIds.back() == requestTransactionId); +ASSERT(callback.excludedPackets.back() == nullptr); +ASSERT(established->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +ASSERT(!handler.isAddbaResponsePending(peer1, 3)); +ASSERT(handler.processReceivedAddbaResp(success, &originatorPolicy, &callback).establishedAgreement == nullptr); + +// IEEE Std 802.11-2024, 10.25.2 and Figure 11-32: a successful matching +// response establishes the agreement even when local policy rejects the +// negotiated parameters; the originator immediately deletes its local state +// and sends END_BA on a best-effort basis. +{ + TestOriginatorHandler vetoHandler; + TestCallback vetoCallback; + TestOriginatorPolicy vetoPolicy; + vetoPolicy.addbaAccepted = false; + Packet vetoTrigger("vetoTrigger"); + auto vetoHeader = makeQosHeader(peer3, 6, SequenceNumberCyclic(30)); + vetoHandler.processAcknowledgedDataFrame(&vetoTrigger, vetoHeader, &vetoPolicy, &vetoCallback); + auto vetoRequest = dynamicPtrCast(vetoCallback.managementHeader); + auto vetoTransactionId = vetoCallback.managementPacket->getTag()->getTransactionId(); + vetoHandler.processTransmittedAddbaReq(vetoCallback.managementPacket, vetoRequest, &vetoPolicy, &vetoCallback); + auto vetoResponse = makeResponse(peer3, 6, vetoRequest->getDialogToken(), 0); + auto vetoResponseOutcome = vetoHandler.processReceivedAddbaResp(vetoResponse, &vetoPolicy, &vetoCallback); + auto vetoAgreement = vetoResponseOutcome.terminatedAgreement.get(); + ASSERT(vetoAgreement != nullptr); + ASSERT(vetoResponseOutcome.establishedAgreement == nullptr); + ASSERT(vetoResponseOutcome.teardownTransactionId == vetoTransactionId); + ASSERT(vetoAgreement->getIsAddbaResponseReceived()); + ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); + auto vetoDelba = vetoResponseOutcome.teardownDelba; + ASSERT(vetoDelba != nullptr); + ASSERT(vetoDelba->getInitiator()); + ASSERT(vetoDelba->getReceiverAddress() == peer3); + ASSERT(vetoDelba->getTid() == 6); + ASSERT(vetoDelba->getReasonCode() == RC_END_BA); + Packet vetoDelbaPacket("vetoDelba", vetoDelba); + vetoDelbaPacket.addTag()->setTransactionId(vetoResponseOutcome.teardownTransactionId); + ASSERT(vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); + ASSERT(vetoHandler.getRetryDeadline(peer3, 6) > simTime()); + TestCallback vetoRetryCallback; + Packet vetoRetryTrigger("vetoRetryTrigger"); + auto vetoRetryHeader = makeQosHeader(peer3, 6, SequenceNumberCyclic(31)); + vetoHandler.processAcknowledgedDataFrame(&vetoRetryTrigger, vetoRetryHeader, &vetoPolicy, &vetoRetryCallback); + ASSERT(vetoRetryCallback.managementPacket == nullptr); + vetoHandler.allowRetryNow(peer3, 6); + auto obsoleteVetoTransactionId = vetoHandler.processAcknowledgedDataFrame(&vetoRetryTrigger, vetoRetryHeader, &vetoPolicy, &vetoRetryCallback); + ASSERT(obsoleteVetoTransactionId == vetoTransactionId); + ASSERT(vetoRetryCallback.managementPacket != nullptr); + auto newerAgreement = vetoHandler.getAgreement(peer3, 6); + ASSERT(newerAgreement != nullptr); + ASSERT(newerAgreement->getTransactionId() != vetoTransactionId); + ASSERT(!vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + ASSERT(!vetoHandler.processAbortedDelba(&vetoDelbaPacket, &vetoCallback)); + ASSERT(!vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + auto vetoTerminated = vetoHandler.processTransmittedDelba(&vetoDelbaPacket, &vetoCallback); + ASSERT(vetoTerminated == nullptr); + ASSERT(vetoHandler.getAgreement(peer3, 6) == newerAgreement); +} + +Packet rejectedTrigger("rejectedTrigger"); +auto rejectedHeader = makeQosHeader(peer2, 4, SequenceNumberCyclic(10)); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +auto rejectedRequest = dynamicPtrCast(callback.managementHeader); +handler.processTransmittedAddbaReq(callback.managementPacket, rejectedRequest, &originatorPolicy, &callback); +auto rejection = makeResponse(peer2, 4, rejectedRequest->getDialogToken(), 1); +ASSERT(handler.processReceivedAddbaResp(rejection, &originatorPolicy, &callback).establishedAgreement == nullptr); +ASSERT(handler.getAgreement(peer2, 4) == nullptr); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer2, 4) == nullptr); +handler.allowRetryNow(peer2, 4); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer2, 4)); + +Packet timeoutTrigger("timeoutTrigger"); +auto timeoutHeader = makeQosHeader(peer3, 5, SequenceNumberCyclic(20)); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +auto timedRequest = dynamicPtrCast(callback.managementHeader); +auto timedRequestPacket = callback.managementPacket; +auto timedTransactionId = timedRequestPacket->getTag()->getTransactionId(); +handler.processTransmittedAddbaReq(timedRequestPacket, timedRequest, &originatorPolicy, &callback); +handler.expireNow(peer3, 5); +handler.addbaResponseTimeoutExpired(&originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +ASSERT(callback.cancelledTransactionIds.back() == timedTransactionId); +ASSERT(callback.excludedPackets.back() == nullptr); +handler.processTransmittedAddbaReq(timedRequestPacket, timedRequest, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +auto staleResponse = makeResponse(peer3, 5, timedRequest->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(staleResponse, &originatorPolicy, &callback).establishedAgreement == nullptr); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +handler.allowRetryNow(peer3, 5); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer3, 5)); + +// All simultaneously expired agreements are erased before cancellation can +// reenter the MAC and query or mutate transaction state. +{ + TestOriginatorHandler timeoutHandler; + TestOriginatorPolicy timeoutPolicy; + TestTimeoutCancellationCallback timeoutCallback; + timeoutCallback.handler = &timeoutHandler; + timeoutCallback.expiredAgreementIds = { { peer1, 1 }, { peer2, 2 } }; + + Packet firstTimeoutTrigger("firstTimeoutTrigger"); + auto firstTimeoutHeader = makeQosHeader(peer1, 1, SequenceNumberCyclic(40)); + timeoutHandler.processAcknowledgedDataFrame(&firstTimeoutTrigger, firstTimeoutHeader, &timeoutPolicy, &timeoutCallback); + auto firstTimeoutRequestPacket = timeoutCallback.managementPacket; + auto firstTimeoutRequest = dynamicPtrCast(timeoutCallback.managementHeader); + auto firstTimeoutTransactionId = firstTimeoutRequestPacket->getTag()->getTransactionId(); + timeoutHandler.processTransmittedAddbaReq(firstTimeoutRequestPacket, firstTimeoutRequest, &timeoutPolicy, &timeoutCallback); + + Packet secondTimeoutTrigger("secondTimeoutTrigger"); + auto secondTimeoutHeader = makeQosHeader(peer2, 2, SequenceNumberCyclic(50)); + timeoutHandler.processAcknowledgedDataFrame(&secondTimeoutTrigger, secondTimeoutHeader, &timeoutPolicy, &timeoutCallback); + auto secondTimeoutRequestPacket = timeoutCallback.managementPacket; + auto secondTimeoutRequest = dynamicPtrCast(timeoutCallback.managementHeader); + auto secondTimeoutTransactionId = secondTimeoutRequestPacket->getTag()->getTransactionId(); + timeoutHandler.processTransmittedAddbaReq(secondTimeoutRequestPacket, secondTimeoutRequest, &timeoutPolicy, &timeoutCallback); + + timeoutHandler.expireNow(peer1, 1); + timeoutHandler.expireNow(peer2, 2); + timeoutHandler.addbaResponseTimeoutExpired(&timeoutPolicy, &timeoutCallback); + ASSERT(timeoutCallback.allExpiredAgreementsGoneBeforeCallbacks); + ASSERT(timeoutCallback.cancelledTransactionIds.size() == 2); + ASSERT(std::find(timeoutCallback.cancelledTransactionIds.begin(), timeoutCallback.cancelledTransactionIds.end(), firstTimeoutTransactionId) != timeoutCallback.cancelledTransactionIds.end()); + ASSERT(std::find(timeoutCallback.cancelledTransactionIds.begin(), timeoutCallback.cancelledTransactionIds.end(), secondTimeoutTransactionId) != timeoutCallback.cancelledTransactionIds.end()); + ASSERT(timeoutCallback.addbaDeadline == SIMTIME_MAX); +} + +// Transaction cancellation leaves packets borrowed by the active frame +// sequence under that sequence's ownership, including RTS-protected packets. +{ + const uint64_t transactionId = 900; + TestPacketQueue pendingQueue; + TestInProgressFrames activeFrames; + auto directRequest = makeShared(); + auto directPacket = new Packet("activeAddbaRequest", directRequest); + directPacket->addTag()->setTransactionId(transactionId); + auto protectedRequest = makeShared(); + auto protectedPacket = new Packet("rtsProtectedAddbaRequest", protectedRequest); + protectedPacket->addTag()->setTransactionId(transactionId); + activeFrames.addFrame(directPacket); + activeFrames.addFrame(protectedPacket); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &activeFrames; + TestEdca edca; + edca.edcaf = &edcaf; + auto frameSequenceHandler = new TestFrameSequenceHandler(); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &activeFrames, nullptr, nullptr, nullptr, nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("rts", makeShared()), 0)); + TestHcf hcf; + hcf.configure(&edca); + hcf.configureFrameSequenceHandler(frameSequenceHandler); + hcf.cancelTransaction(transactionId); + ASSERT(activeFrames.getLength() == 2); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; +} + + +// A transaction frame that is not referenced by an active sequence is removed +// from in-progress ownership and reclaimed immediately, without entering the +// deferred dropped-frame list. +{ + const uint64_t transactionId = 901; + TestPacketQueue pendingQueue; + QosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setSequenceNumber(SequenceNumberCyclic(31)); + auto packet = new Packet("idleAddbaRequest", request); + packet->addTag()->setTransactionId(transactionId); + inProgressFrames.addOwnedFrame(packet); + ackHandler.frameGotInProgress(request); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configure(&edca); + TestPacketDropSignalListener dropListener; + hcf.subscribe(packetDroppedSignal, &dropListener); + hcf.cancelTransaction(transactionId); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 0); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// An unsent transaction has no response deadline and is retired by a typed drop callback. +Packet unsentTrigger("unsentTrigger"); +auto unsentHeader = makeQosHeader(peer2, 5, SequenceNumberCyclic(40)); +handler.processAcknowledgedDataFrame(&unsentTrigger, unsentHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer2, 5)); + +// Real observed pending queues treat REMOVED as terminal for an unsent tagged +// ADDBA Request, while DEQUEUED is only ownership transfer. The held same-TID +// data frame becomes eligible exactly once after terminal cleanup. +auto verifyObservedPendingRemoval = [&](queueing::IPacketQueue::PacketRemovalReason removalReason, bool removeAll) { + auto observedHandler = new TestOriginatorHandler(); + TestOriginatorQosMacDataService observedDataService; + observedDataService.setFrameEligibilityFunction([observedHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return observedHandler->isAddbaRequestPending(packet, addbaReq); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !observedHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + TestPacketQueue observedQueue; + TestAckHandler observedAckHandler; + TestInProgressFrames observedInProgressFrames; + observedInProgressFrames.configure(&observedDataService, &observedAckHandler, &observedQueue); + TestEdcaf observedEdcaf; + observedEdcaf.pendingQueue = &observedQueue; + observedEdcaf.inProgressFrames = &observedInProgressFrames; + TestEdca observedEdca; + observedEdca.edcaf = &observedEdcaf; + TestHcf observedHcf; + observedHcf.configureEligibilityIndex(&observedEdca, &observedDataService); + auto observedRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy observedRecipientPolicy; + observedHcf.configureBlockAckHandlers(observedHandler, &originatorPolicy, observedRecipientHandler, &observedRecipientPolicy); + observedHcf.delegateDroppedSetupHandling = true; + observedHcf.observePendingQueue(&observedQueue); + TestCallback observedCallback; + Packet observedTrigger("observedTrigger"); + auto observedHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(60)); + observedHandler->processAcknowledgedDataFrame(&observedTrigger, observedHeader, &originatorPolicy, &observedCallback); + auto observedRequestPacket = observedCallback.managementPacket; + auto observedRequest = dynamicPtrCast(observedCallback.managementHeader); + observedCallback.forgetManagementPacket(observedRequestPacket); + auto heldDataPacket = new Packet("heldSameTidData", makeQosHeader(peer2, 6, SequenceNumberCyclic(61))); + observedQueue.enqueuePacket(observedRequestPacket); + observedQueue.enqueuePacket(heldDataPacket); + observedHcf.trackFrame(observedRequestPacket, AC_BK); + observedHcf.trackFrame(heldDataPacket, AC_BK); + ASSERT(observedHcf.getNumEligiblePendingFrames(AC_BK) == 1); + if (removalReason == queueing::IPacketQueue::PacketRemovalReason::DEQUEUED) { + auto dequeuedPacket = observedQueue.dequeuePacket(); + ASSERT(dequeuedPacket == observedRequestPacket); + ASSERT(observedHandler->isAddbaResponsePending(peer2, 6)); + ASSERT(observedHcf.numResumedEligibleChannelAccess == 0); + observedHandler->processDroppedAddbaReq(dequeuedPacket, observedRequest, &originatorPolicy, &observedCallback); + delete dequeuedPacket; + } + else { + if (removeAll) + observedQueue.removeAllPackets(); + else + observedQueue.removePacket(observedRequestPacket); + ASSERT(observedHandler->getAgreement(peer2, 6) == nullptr); + ASSERT(observedHcf.getNumEligiblePendingFrames(AC_BK) == (removeAll ? 0 : 1)); + ASSERT(observedHcf.numResumedEligibleChannelAccess == 1); + if (!removeAll) + observedQueue.removePacket(heldDataPacket); + delete observedRequestPacket; + } + delete heldDataPacket; +}; +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::REMOVED, false); +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::REMOVED, true); +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::DEQUEUED, false); + +ASSERT(callback.addbaDeadline == SIMTIME_MAX); +auto unsentRequest = dynamicPtrCast(callback.managementHeader); +auto unsentRequestPacket = callback.managementPacket; +auto unsentTransactionId = unsentRequestPacket->getTag()->getTransactionId(); +handler.processDroppedAddbaReq(unsentRequestPacket, unsentRequest, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer2, 5) == nullptr); +ASSERT(callback.cancelledTransactionIds.back() == unsentTransactionId); +ASSERT(callback.excludedPackets.back() == unsentRequestPacket); + +// A DELBA that terminates a pending transaction also cancels its tagged ADDBA +// Request and recomputes the response timer. +{ + TestOriginatorHandler receivedDelbaHandler; + TestCallback receivedDelbaCallback; + Packet receivedDelbaTrigger("receivedDelbaTrigger"); + auto receivedDelbaTriggerHeader = makeQosHeader(peer1, 2, SequenceNumberCyclic(60)); + receivedDelbaHandler.processAcknowledgedDataFrame(&receivedDelbaTrigger, receivedDelbaTriggerHeader, &originatorPolicy, &receivedDelbaCallback); + auto receivedDelbaTransactionId = receivedDelbaCallback.managementPacket->getTag()->getTransactionId(); + auto receivedDelba = makeShared(); + receivedDelba->setTransmitterAddress(peer1); + receivedDelba->setTid(2); + auto terminatedAgreement = receivedDelbaHandler.processReceivedDelba(receivedDelba, &originatorPolicy, &receivedDelbaCallback); + ASSERT(terminatedAgreement != nullptr); + if (receivedDelbaHandler.getAgreement(peer1, 2) != nullptr || receivedDelbaCallback.cancelledTransactionIds.size() != 1 || receivedDelbaCallback.cancelledTransactionIds.back() != receivedDelbaTransactionId) + throw cRuntimeError("Received DELBA did not cancel the pending ADDBA transaction"); + ASSERT(receivedDelbaHandler.getAgreement(peer1, 2) == nullptr); + ASSERT(receivedDelbaCallback.cancelledTransactionIds.back() == receivedDelbaTransactionId); + ASSERT(receivedDelbaCallback.excludedPackets.back() == nullptr); + ASSERT(receivedDelbaCallback.addbaDeadline == SIMTIME_MAX); +} +{ + TestOriginatorHandler transmittedDelbaHandler; + TestCallback transmittedDelbaCallback; + Packet transmittedDelbaTrigger("transmittedDelbaTrigger"); + auto transmittedDelbaTriggerHeader = makeQosHeader(peer2, 2, SequenceNumberCyclic(70)); + transmittedDelbaHandler.processAcknowledgedDataFrame(&transmittedDelbaTrigger, transmittedDelbaTriggerHeader, &originatorPolicy, &transmittedDelbaCallback); + auto transmittedDelbaTransactionId = transmittedDelbaCallback.managementPacket->getTag()->getTransactionId(); + auto transmittedDelba = makeShared(); + transmittedDelba->setReceiverAddress(peer2); + transmittedDelba->setTid(2); + Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); + auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback); + ASSERT(terminatedPendingAgreement != nullptr); + ASSERT(terminatedPendingAgreement->isPending()); + if (transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr || transmittedDelbaCallback.cancelledTransactionIds.size() != 1 || transmittedDelbaCallback.cancelledTransactionIds.back() != transmittedDelbaTransactionId) + throw cRuntimeError("Transmitted DELBA did not cancel the pending ADDBA transaction"); + ASSERT(transmittedDelbaHandler.getAgreement(peer2, 2) == nullptr); + ASSERT(transmittedDelbaCallback.cancelledTransactionIds.back() == transmittedDelbaTransactionId); + ASSERT(transmittedDelbaCallback.excludedPackets.back() == nullptr); + ASSERT(transmittedDelbaCallback.addbaDeadline == SIMTIME_MAX); +} + +// Fragmentation preserves the local transaction identity on every MPDU, and +// typed cancellation removes all exact siblings while retaining the dropped +// fragment and an unrelated transaction. +{ + TestOriginatorHandler fragmentedHandler; + TestCallback fragmentedCallback; + Packet fragmentedTrigger("fragmentedTrigger"); + auto fragmentedTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(50)); + fragmentedHandler.processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); + auto unfragmentedRequestPacket = fragmentedCallback.managementPacket; + auto fragmentedTransactionId = unfragmentedRequestPacket->getTag()->getTransactionId(); + fragmentedCallback.forgetManagementPacket(unfragmentedRequestPacket); + unfragmentedRequestPacket->insertAtBack(makeShared(B(4))); + unfragmentedRequestPacket->insertAtBack(makeShared()); + TestFragmentationPolicy fragmentationPolicy; + TestOriginatorQosMacDataService fragmentationDataService; + fragmentationDataService.enableFragmentation(&fragmentationPolicy); + auto fragments = fragmentationDataService.fragment(unfragmentedRequestPacket); + ASSERT(fragments != nullptr); + ASSERT(fragments->size() == 2); + for (auto fragment : *fragments) + ASSERT(fragment->getTag()->getTransactionId() == fragmentedTransactionId); + auto unrelatedPacket = new Packet("unrelatedTransaction", makeShared(B(1))); + unrelatedPacket->addTag()->setTransactionId(fragmentedTransactionId + 1); + auto droppedFragment = fragments->at(0); + auto siblingFragment = fragments->at(1); + TestPacketQueue fragmentedPendingQueue; + fragmentedPendingQueue.packets = { droppedFragment, siblingFragment, unrelatedPacket }; + TestInProgressFrames fragmentedInProgressFrames; + TestEdcaf fragmentedEdcaf; + fragmentedEdcaf.pendingQueue = &fragmentedPendingQueue; + fragmentedEdcaf.inProgressFrames = &fragmentedInProgressFrames; + TestEdca fragmentedEdca; + fragmentedEdca.edcaf = &fragmentedEdcaf; + TestHcf fragmentedHcf; + fragmentedHcf.configure(&fragmentedEdca); + auto droppedRequest = droppedFragment->peekAtFront(); + fragmentedHandler.processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); + ASSERT(fragmentedHandler.getAgreement(peer1, 7) == nullptr); + ASSERT(fragmentedPendingQueue.getNumPackets() == 2); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), droppedFragment) != fragmentedPendingQueue.packets.end()); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), siblingFragment) == fragmentedPendingQueue.packets.end()); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), unrelatedPacket) != fragmentedPendingQueue.packets.end()); + fragmentedPendingQueue.removePacket(droppedFragment); + fragmentedPendingQueue.removePacket(unrelatedPacket); + delete droppedFragment; + delete fragments; + delete unrelatedPacket; +} + +{ + TestOriginatorHandler invalidTimeoutHandler; + TestOriginatorPolicy invalidTimeoutPolicy; + TestCallback invalidTimeoutCallback; + invalidTimeoutPolicy.addbaResponseTimeout = 0; + Packet invalidTimeoutTrigger("invalidTimeoutTrigger"); + auto invalidTimeoutHeader = makeQosHeader(peer2, 1, SequenceNumberCyclic(30)); + bool threw = false; + try { + invalidTimeoutHandler.processAcknowledgedDataFrame(&invalidTimeoutTrigger, invalidTimeoutHeader, &invalidTimeoutPolicy, &invalidTimeoutCallback); + auto invalidRequest = dynamicPtrCast(invalidTimeoutCallback.managementHeader); + invalidTimeoutHandler.processTransmittedAddbaReq(invalidTimeoutCallback.managementPacket, invalidRequest, &invalidTimeoutPolicy, &invalidTimeoutCallback); + } + catch (cRuntimeError&) { + threw = true; + } + ASSERT(threw); +} + +// A direct IPacketQueue implementation without OMNeT++ signals cannot leave +// the index stale because ownership departures use the typed queue callback. +{ + TestOriginatorQosMacDataService customQueueDataService; + TestPacketQueue customQueue; + TestAckHandler customQueueAckHandler; + TestInProgressFrames customQueueInProgressFrames; + customQueueInProgressFrames.configure(&customQueueDataService, &customQueueAckHandler, &customQueue); + TestEdcaf customQueueEdcaf; + customQueueEdcaf.pendingQueue = &customQueue; + customQueueEdcaf.inProgressFrames = &customQueueInProgressFrames; + TestEdca customQueueEdca; + customQueueEdca.edcaf = &customQueueEdcaf; + TestHcf customQueueHcf; + customQueueHcf.configureEligibilityIndex(&customQueueEdca, &customQueueDataService); + customQueueHcf.observePendingQueue(&customQueue); + customQueueHcf.rebuildEligibilityIndex(); + auto customPacket = new Packet("customQueuePacket", makeShared(B(1))); + customQueueHcf.trackFrame(customPacket, AC_BK); + customQueue.enqueuePacket(customPacket); + if (customQueueHcf.getNumEligiblePendingFrames(AC_BK) != 1) + throw cRuntimeError("Custom queue enqueue was not indexed"); + auto customDequeuedPacket = customQueue.dequeuePacket(); + if (customDequeuedPacket != customPacket || customQueueHcf.getNumTrackedPendingFrames() != 0) + throw cRuntimeError("Custom queue departure callback left the index stale"); + delete customDequeuedPacket; +} + +// Top-level queue lifecycle signals maintain the same index incrementally; +// destructive drops are distinguished from ordinary departures. +{ + TestOriginatorQosMacDataService indexedDataService; + indexedDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + QosAckHandler indexedAckHandler; + TestInProgressFrames indexedInProgressFrames; + indexedInProgressFrames.configure(&indexedDataService, &indexedAckHandler); + TestEdcaf indexedEdcaf; + TestEdca indexedEdca; + indexedEdca.edcaf = &indexedEdcaf; + indexedEdcaf.inProgressFrames = &indexedInProgressFrames; + auto overflowQueue = createPacketQueue("indexedOverflowQueue", 1, "inet::queueing::PacketAtCollectionEndDropper"); + indexedEdcaf.pendingQueue = overflowQueue; + TestHcf indexedHcf; + indexedHcf.configureEligibilityIndex(&indexedEdca, &indexedDataService); + indexedHcf.observePendingQueue(overflowQueue); + indexedHcf.rebuildEligibilityIndex(); + auto retainedPacket = new Packet("indexedRetainedPacket", makeShared(B(1))); + indexedHcf.trackFrame(retainedPacket, AC_BK); + overflowQueue->pushPacket(retainedPacket, nullptr); + auto droppedArrival = new Packet("indexedDroppedArrival", makeShared(B(1))); + indexedHcf.trackFrame(droppedArrival, AC_BK); + overflowQueue->pushPacket(droppedArrival, nullptr); + if (indexedHcf.getNumTrackedPendingFrames() != 1 || indexedHcf.getNumEligiblePendingFrames(AC_BK) != 1) + throw cRuntimeError("Eligibility index did not account for overflow"); + auto selectedPacket = overflowQueue->dequeuePacket([](const Packet *) { return true; }); + if (selectedPacket != retainedPacket || indexedHcf.getNumTrackedPendingFrames() != 0) + throw cRuntimeError("Eligibility index did not account for selected dequeue"); + bool hasFrameAfterSelectedDequeue = indexedHcf.hasFrame(AC_BK); + if (hasFrameAfterSelectedDequeue) + throw cRuntimeError("Eligibility index reports a frame after selected dequeue"); + take(selectedPacket); + delete selectedPacket; + overflowQueue->callFinish(); + overflowQueue->deleteModule(); +} + // Predicate extraction follows each provider's scheduling policy instead of // the compound collection's gate-order enumeration. { @@ -478,6 +1704,367 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const gateModule->deleteModule(); leafQueue->deleteModule(); } + +// Every A-MSDU member is extracted through the scheduler, so provider +// accounting runs for all policy-selected subframes. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstSubframe = makeAggregatePacket("schedulerAggregateFirst", SequenceNumberCyclic(10)); + auto secondSubframe = makeAggregatePacket("schedulerAggregateSecond", SequenceNumberCyclic(11)); + firstQueue.enqueuePacket(firstSubframe); + secondQueue.enqueuePacket(secondSubframe); + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + if (frames == nullptr || frames->size() != 1 || scheduler.getNumProcessedPackets() != 2 || !firstQueue.isEmpty() || !secondQueue.isEmpty()) + throw cRuntimeError("A-MSDU extraction bypassed scheduler processing"); + service.release(frames->front()); + delete frames->front(); + delete frames; +} + +// A-MSDU validation discovers selected members by identity without enumerating +// an unrelated trailing backlog through the packet collection API. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue queue; + auto firstSubframe = makeAggregatePacket("backlogAggregateFirst", SequenceNumberCyclic(14)); + auto secondSubframe = makeAggregatePacket("backlogAggregateSecond", SequenceNumberCyclic(15)); + queue.packets = { firstSubframe, secondSubframe }; + for (int i = 0; i < 128; i++) + queue.packets.push_back(makeAggregatePacket("backlogAggregateTail", SequenceNumberCyclic(16 + i))); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&queue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(queue.getNumPackets() == 128); + ASSERT(queue.numGetPacketCalls == 0); + service.release(frames->front()); + delete frames->front(); + delete frames; + for (auto packet : queue.packets) + delete packet; + queue.packets.clear(); +} + +// The built-in policy is anchored on the provider-selected candidate. Held +// enumeration members do not hide later compatible frames, while an oversized +// anchor falls back to ordinary dequeue without aggregating its followers. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue queue; + auto anchor = makeAggregatePacket("basicPolicyAnchor", SequenceNumberCyclic(20), 8); + auto held = makeAggregatePacket("basicPolicyHeld", SequenceNumberCyclic(21), 8); + auto compatible = makeAggregatePacket("basicPolicyCompatible", SequenceNumberCyclic(22), 8); + auto heldHeader = held->removeAtFront(); + heldHeader->setTid(2); + held->insertAtFront(heldHeader); + queue.packets = { anchor, held, compatible }; + TestBasicMsduAggregationPolicy policy; + policy.configure(B(4065)); + auto selected = policy.computeAggregateFrames(&queue, anchor, [held](const Packet *packet) { return packet != held; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == anchor); + ASSERT(selected->at(1) == compatible); + delete selected; + auto oversizedAnchor = makeAggregatePacket("basicPolicyOversizedAnchor", SequenceNumberCyclic(23), 64); + auto smallFollower1 = makeAggregatePacket("basicPolicySmallFollower1", SequenceNumberCyclic(24), 1); + auto smallFollower2 = makeAggregatePacket("basicPolicySmallFollower2", SequenceNumberCyclic(25), 1); + queue.packets = { oversizedAnchor, smallFollower1, smallFollower2 }; + policy.configure(B(30)); + ASSERT(policy.computeAggregateFrames(&queue, oversizedAnchor, [](const Packet *) { return true; }) == nullptr); + auto prefix = makeAggregatePacket("basicPolicyPrefix", SequenceNumberCyclic(26), 8); + auto middleAnchor = makeAggregatePacket("basicPolicyMiddleAnchor", SequenceNumberCyclic(27), 8); + auto suffix = makeAggregatePacket("basicPolicySuffix", SequenceNumberCyclic(28), 8); + queue.packets = { prefix, middleAnchor, suffix }; + policy.configure(B(4065)); + selected = policy.computeAggregateFrames(&queue, middleAnchor, [prefix](const Packet *packet) { return packet != prefix; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == middleAnchor); + ASSERT(selected->at(1) == suffix); + delete selected; + auto capacityAnchor = makeAggregatePacket("basicPolicyCapacityAnchor", SequenceNumberCyclic(29), 8); + auto oversizedMiddle = makeAggregatePacket("basicPolicyOversizedMiddle", SequenceNumberCyclic(30), 64); + auto smallSuffix = makeAggregatePacket("basicPolicySmallSuffix", SequenceNumberCyclic(31), 1); + queue.packets = { capacityAnchor, oversizedMiddle, smallSuffix }; + policy.configure(B(40)); + ASSERT(policy.computeAggregateFrames(&queue, capacityAnchor, [](const Packet *) { return true; }) == nullptr); + queue.packets.clear(); + for (auto packet : { anchor, held, compatible, oversizedAnchor, smallFollower1, smallFollower2, prefix, middleAnchor, suffix, capacityAnchor, oversizedMiddle, smallSuffix }) + delete packet; +} + +// Contract validation owns both already-extracted frames and an unexpected +// non-null provider result, so a caught provider error leaves only the frame +// which was never extracted in the queue. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestWrongDequeueQueue queue; + auto first = makeAggregatePacket("badProviderFirst", SequenceNumberCyclic(35)); + auto second = makeAggregatePacket("badProviderSecond", SequenceNumberCyclic(36)); + auto wrong = makeAggregatePacket("badProviderWrong", SequenceNumberCyclic(37)); + queue.packets = { first, second, wrong }; + queue.wrongPacket = wrong; + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { first, second }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + bool contractError = false; + try { + service.extractFramesToTransmit(&queue); + } + catch (cRuntimeError& error) { + contractError = std::string(error.what()).find("no longer available") != std::string::npos; + } + ASSERT(contractError); + ASSERT(queue.packets.size() == 1); + ASSERT(queue.packets.front() == second); + queue.packets.clear(); + delete second; +} + +// Returning an already-extracted pointer for a later exact dequeue is also a +// provider contract violation, but the cleanup guard must adopt it only once. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestRepeatedDequeueQueue queue; + auto first = makeAggregatePacket("repeatedProviderFirst", SequenceNumberCyclic(38)); + auto second = makeAggregatePacket("repeatedProviderSecond", SequenceNumberCyclic(39)); + queue.packets = { first, second }; + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { first, second }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + bool contractError = false; + try { + service.extractFramesToTransmit(&queue); + } + catch (cRuntimeError& error) { + contractError = std::string(error.what()).find("no longer available") != std::string::npos; + } + ASSERT(contractError); + ASSERT(queue.packets.size() == 1); + ASSERT(queue.packets.front() == second); + queue.packets.clear(); + delete second; +} + +// Reverse-priority scheduling may select an anchor after an eligible packet in +// collection enumeration. The built-in policy must never wrap and append that +// earlier packet after the anchor. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto earlierPacket = makeAggregatePacket("reverseEarlier", SequenceNumberCyclic(32)); + auto reverseAnchor = makeAggregatePacket("reverseAnchor", SequenceNumberCyclic(33)); + auto reverseSuffix = makeAggregatePacket("reverseSuffix", SequenceNumberCyclic(34)); + firstQueue.packets = { earlierPacket }; + secondQueue.packets = { reverseAnchor, reverseSuffix }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + auto candidate = compoundQueue.findPacket([](const Packet *) { return true; }); + ASSERT(candidate == reverseAnchor); + TestBasicMsduAggregationPolicy policy; + policy.configure(B(4065)); + auto selected = policy.computeAggregateFrames(&compoundQueue, candidate, [](const Packet *) { return true; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == reverseAnchor); + ASSERT(selected->at(1) == reverseSuffix); + delete selected; + firstQueue.packets.clear(); + secondQueue.packets.clear(); + delete earlierPacket; + delete reverseAnchor; + delete reverseSuffix; +} + +// A flow provider may expose an enumeration order different from its +// scheduling behavior. Candidate-aware aggregation still extracts every +// selected member through the provider and runs its processing exactly once. +{ + auto leafQueue = createPacketQueue("aggregationFlowLeaf"); + auto flowModule = cModuleType::get("inet.queueing.common.BackPressureBarrier")->create("aggregationFlow", this); + leafQueue->gate("out")->connectTo(flowModule->gate("in")); + flowModule->callInitialize(); + TestSignalListener flowListener; + flowModule->subscribe(packetPulledOutSignal, &flowListener); + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto firstSubframe = makeAggregatePacket("flowAggregateFirst", SequenceNumberCyclic(12)); + auto secondSubframe = makeAggregatePacket("flowAggregateSecond", SequenceNumberCyclic(13)); + leafQueue->pushPacket(firstSubframe, nullptr); + leafQueue->pushPacket(secondSubframe, nullptr); + auto flowCollection = check_and_cast(flowModule); + auto flowExtractor = check_and_cast(flowModule); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(flowCollection, flowExtractor); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + if (frames == nullptr || frames->size() != 1 || !frames->front()->peekAtFront()->getAMsduPresent() || aggregationPolicy.numCalls != 1 || flowListener.numSignals != 2 || leafQueue->getNumPackets() != 0) + throw cRuntimeError("A-MSDU aggregation bypassed PacketFlowBase extraction"); + service.release(frames->front()); + delete frames->front(); + delete frames; + flowModule->unsubscribe(packetPulledOutSignal, &flowListener); + flowModule->callFinish(); + flowModule->deleteModule(); + leafQueue->callFinish(); + leafQueue->deleteModule(); +} + +// End-to-end compound extraction uses the provider's reverse-priority choice +// as the aggregate anchor while leaving an ineligible higher-level frame held. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto heldPacket = makeAggregatePacket("compoundHeldPacket", SequenceNumberCyclic(1)); + auto selectedPacket = makeAggregatePacket("compoundSelectedPacket", SequenceNumberCyclic(2)); + auto companionPacket = makeAggregatePacket("compoundCompanionPacket", SequenceNumberCyclic(3)); + firstQueue.packets = { heldPacket }; + secondQueue.packets = { selectedPacket, companionPacket }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { selectedPacket, companionPacket }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + service.setFrameEligibilityFunction([heldPacket](const Packet *packet) { return packet != heldPacket; }); + auto frames = service.extractFramesToTransmit(&compoundQueue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(aggregationPolicy.numCalls == 1); + ASSERT(firstQueue.getNumPackets() == 1); + ASSERT(secondQueue.isEmpty()); + service.release(frames->front()); + delete frames->front(); + delete frames; + firstQueue.removePacket(heldPacket); + delete heldPacket; +} + +// A-MSDU aggregation may select multiple WRR members, but each exact dequeue +// must still update WRR accounting. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto selectedPacket = makeAggregatePacket("wrrAggregateSelectedPacket", SequenceNumberCyclic(3)); + auto sameInputPacket = makeAggregatePacket("wrrAggregateSameInputPacket", SequenceNumberCyclic(4)); + auto otherInputPacket = makeAggregatePacket("wrrAggregateOtherInputPacket", SequenceNumberCyclic(5)); + firstQueue.packets = { selectedPacket, sameInputPacket }; + secondQueue.packets = { otherInputPacket }; + TestWrrScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { 1, 1 }, { 1, 1 }); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + ASSERT(compoundQueue.getPacket(0) == selectedPacket); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { selectedPacket, sameInputPacket }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(aggregationPolicy.numCalls == 1); + ASSERT(firstQueue.isEmpty()); + ASSERT(secondQueue.getNumPackets() == 1); + ASSERT(scheduler.getNumProcessedPackets() == 2); + ASSERT(scheduler.getBucket(0) == 0); + ASSERT(scheduler.getBucket(1) == 1); + service.release(frames->front()); + delete frames->front(); + delete frames; + secondQueue.removePacket(otherInputPacket); + delete otherInputPacket; +} // A real queue with a downstream collector predicate-dequeues a non-front // packet, records its residence and semantic pull once, and animates it to the // collector while retaining the front packet. @@ -596,6 +2183,27 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const gateModule->deleteModule(); leafQueueModule->deleteModule(); } + +// Ordinary queue overflow must not trigger channel-access re-evaluation, while +// cancelling a dropped setup transaction must immediately release held traffic. +{ + TestHcf droppedSetupHcf; + Packet droppedSetupPacket("droppedSetupPacket", makeShared(B(1))); + droppedSetupHcf.dropPacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 1); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 0); + droppedSetupHcf.droppedSetupCancelled = true; + droppedSetupHcf.dropPacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 2); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 1); + droppedSetupHcf.removePacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 3); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 2); + droppedSetupHcf.dequeuePacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 3); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 2); +} + // The typed queue callback covers every non-destructive ownership departure, // including bulk removal, exactly once at the queue boundary. { @@ -942,6 +2550,24 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const secondQueue->deleteModule(); bufferModule->deleteModule(); } +// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. +RecipientBlockAckAgreementHandler immediateRecipientHandler; +TestRecipientPolicy recipientPolicy; +TestCallback recipientCallback; +auto recipientRequest = makeShared(); +recipientRequest->setTransmitterAddress(peer1); +recipientRequest->setTid(6); +recipientRequest->setDialogToken(77); +recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(0)); +recipientRequest->setBufferSize(64); +recipientRequest->setBlockAckPolicy(1); +recipientRequest->setBlockAckTimeoutValue(0); +immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback); +auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); +ASSERT(acceptedResponse->getDialogToken() == 77); +ASSERT(acceptedResponse->getStatusCode() == 0); + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; + %contains: stdout ADDBA transaction and negotiated SSN integrity checks passed. From 9803bb3307a867228c48071e38daad80efe0c9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:39:54 +0200 Subject: [PATCH 30/91] ieee80211: add+fix: reset recipient state on renegotiation A successful replacement ADDBA agreement must reset the recipient's reordering and receive state, while a duplicate request must not repeat that state transition. Preserve the response body for the matching retry and exercise successful and timed-out transactions in a focused example. Add the three MacQosWithTransactionalBlockAck expectations measured with the corrected BAR timing on rebased master 7287f347aa. This example first exists here, so its initial baseline belongs here. The six existing selected Block Ack cases preserve their preceding values. The transactional values remain stable through the final source checkpoint. The user approved these exact refreshed expectations after the 2026-09-15 attribution report. Scoped debug verification uses run 0 and all declared ingredients. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211 | behavior.add+change.fix | fingerprint test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 42 +- doc/src/migration-guide/index.rst | 11 + examples/wireless/qos/omnetpp.ini | 23 + .../mac/blockack/RecipientBlockAckAgreement.h | 2 - .../RecipientBlockAckAgreementHandler.cc | 117 +- .../RecipientBlockAckAgreementHandler.h | 15 +- .../blockackreordering/BlockAckReordering.cc | 9 +- .../blockackreordering/BlockAckReordering.h | 2 +- .../mac/blockackreordering/ReceiveBuffer.cc | 10 + .../mac/blockackreordering/ReceiveBuffer.h | 1 + .../IRecipientBlockAckAgreementHandler.h | 10 +- .../contract/IRecipientQosMacDataService.h | 11 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 54 +- .../ieee80211/mac/coordinationfunction/Hcf.h | 8 +- .../mac/coordinationfunction/Hcf.ned | 2 + .../recipient/RecipientQosMacDataService.cc | 29 +- .../recipient/RecipientQosMacDataService.h | 3 +- tests/fingerprint/examples.csv | 1 + tests/unit/Ieee80211AddbaTransaction_1.test | 1432 ++++++++++++++++- 19 files changed, 1655 insertions(+), 127 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index a4bbc67ea29..5d1e61e9cb1 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -311,15 +311,20 @@ Notable backward incompatible changes are the following: implementations of IPacketExtractor must replace selected-pointer extraction with findPacket(predicate) and dequeuePacket(predicate); predicates may be evaluated repeatedly and must be stable and side-effect free throughout one - logical selection. WrrScheduler, - LabelScheduler, and PriorityScheduler accept ordinary IPassivePacketSource - inputs; they require IPacketCollection only when collection/aggregate access is - used and IPacketExtractor only when predicate extraction is used, reporting the - unsupported operation lazily. + logical selection. WrrScheduler, LabelScheduler, and PriorityScheduler accept + ordinary IPassivePacketSource inputs; they require IPacketCollection only when + collection/aggregate access is used and IPacketExtractor only when predicate + extraction is used, reporting the unsupported operation lazily. PriorityScheduler aggregate queries no longer return -1 when an input lacks IPacketCollection; getNumPackets() and getTotalLength() now report that unsupported operation with cRuntimeError. Update callers that treated -1 as an unknown aggregate size, or connect collection-capable providers. + A-MSDU policies now receive the provider-selected anchor and frame-eligibility + predicate. Additional policy-selected members are removed through exact + predicate dequeues, preserving scheduler/flow accounting without requiring + collection enumeration order to match scheduling order. + BasicMsduAggregationPolicy conservatively considers only collection members + after the anchor and never bypasses an earlier blocked same-flow member. IPacketBuffer::ICallback requires explicit handlePacketDropping() and handlePacketDropped() implementations. The first detaches the packet; the second publishes its departure after all victims of the overload operation @@ -333,6 +338,33 @@ Notable backward incompatible changes are the following: Custom originator agreement handlers must explicitly implement isDelbaPending(). + IOriginatorBlockAckAgreementHandler::processReceivedDelba(), + IOriginatorBlockAckAgreementHandler::processTransmittedDelba(), and + IRecipientBlockAckAgreementHandler::processReceivedDelba() now return the + terminated agreement as a unique_ptr. Originator ADDBA response processing + now returns a typed outcome containing the established agreement and, when + local policy vetoes a successful response, its immediately terminated local + agreement and a best-effort initiator DELBA for Hcf to enqueue after emitting + the Added and Deleted signals. Such DELBAs carry the ADDBA transaction identity + and ordinary data continues with Normal Ack. They remain eligible through the + final fragment; aborting one fragment cancels its siblings. After retry backoff, + a replacement ADDBA setup invalidates an older queued DELBA, so an unreported + disposal cannot suppress setup indefinitely and a delayed frame cannot + terminate a newer peer/TID agreement. The replacement setup reports that + obsolete identity so Hcf also removes all of its queued or in-progress packets. + Originator DELBA transmission handlers now receive the full Packet so this + identity is retained. A transaction-tagged initiator DELBA remains eligible + across MAC retries and is retired only when its final fragment is acknowledged + or the transaction is terminally aborted. Custom handler implementations and + callers must adopt processAcknowledgedDelba() and the boolean + processAbortedDelba() outcome; processTransmittedDelba() still returns the + agreement removed by an untagged DELBA, or null when none was removed. + + OriginatorBlockAckAgreementPolicy now exposes the new `addbaResponseTimeout` + and `addbaRetryBackoff` NED parameters. C++ implementations of + IOriginatorBlockAckAgreementPolicy must replace computeAddbaFailureTimeout() + with getAddbaResponseTimeout() and implement computeAddbaRetryBackoff(). + 6. Originator ADDBA Transactions and Eligible Frames ADDBA response deadlines now begin at actual request transmission. Frames for a diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index e34aef498bc..1d89e27fbcd 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -103,6 +103,17 @@ transaction tag through retries and fragments. Recipient responses must echo the token. An originator accepts only a matching live response; a response using the old default token zero no longer completes a transaction. +Recipient ADDBA Lifecycle +------------------------- + +Update custom recipient handlers and callers to the current +``processReceivedAddbaRequest()`` callback arguments and returned agreement. An accepted +request establishes or replaces recipient state when the response is formed. Reset the +receive/reordering state for a replacement agreement. Replaying a cached duplicate +response must not establish the agreement again or reset its receive window. Use the +returned ownership-bearing teardown result when publishing a deleted-agreement +notification. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/examples/wireless/qos/omnetpp.ini b/examples/wireless/qos/omnetpp.ini index 887e0683fc6..dccf94b1347 100644 --- a/examples/wireless/qos/omnetpp.ini +++ b/examples/wireless/qos/omnetpp.ini @@ -85,3 +85,26 @@ extends = MacQos # radio medium *.radioMedium.sameTransmissionStartTimeCheck = "ignore" + +[Config MacQosWithTransactionalBlockAck] +description = "Exercises successful and timed-out ADDBA transactions" +extends = MacQosWithoutAggregation +sim-time-limit = 3s + +# Use one voice flow so each wireless hop has a single peer/TID data flow. +*.cliHost.numApps = 1 +*.cliHost.app[0].destPort = 5000 +*.cliHost.app[0].packetName = "TransactionalBlockAck" +*.cliHost.app[0].startTime = 1s +*.cliHost.app[0].stopTime = 2s +*.cliHost.app[0].sendInterval = 10ms +*.srvHost.numApps = 1 +*.srvHost.app[0].localPort = 5000 + +# The client-to-AP transaction succeeds. The AP-to-server transaction times +# out, because the server deliberately does not support Block Ack. +*.cliHost.wlan[0].mac.hcf.isBlockAckSupported = true +*.ap.wlan[0].mac.hcf.isBlockAckSupported = true +*.srvHost.wlan[0].mac.hcf.isBlockAckSupported = false +**.mac.hcf.originatorAckPolicy.blockAckReqThreshold = 2 +**.mac.hcf.originatorBlockAckAgreementPolicy.addbaResponseTimeout = 250ms diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 40a4186fead..aa0c9763e9c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,7 +21,6 @@ class INET_API RecipientBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; - bool isAddbaResponseSent = false; simtime_t expirationTime = -1; public: @@ -35,7 +34,6 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } - virtual void addbaResposneSent() { isAddbaResponseSent = true; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } virtual simtime_t getExpirationTime() { return expirationTime; } friend std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 3e599bb4a48..adf02821b4d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -64,29 +64,6 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb scheduleInactivityTimer(agreementHandlerCallback); } -// -// An originator that intends to use the Block Ack mechanism for the transmission of QoS data frames to an -// intended recipient should first check whether the intended recipient STA is capable of participating in Block -// Ack mechanism by discovering and examining its Delayed Block Ack and Immediate Block Ack capability -// bits. If the intended recipient STA is capable of participating, the originator sends an ADDBA Request frame -// indicating the TID for which the Block Ack is being set up. -// -RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::addAgreement(const Ptr& addbaReq) -{ - MacAddress originatorAddr = addbaReq->getTransmitterAddress(); - auto id = std::make_pair(originatorAddr, addbaReq->getTid()); - auto it = blockAckAgreements.find(id); - if (it == blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = new RecipientBlockAckAgreement(originatorAddr, addbaReq->getTid(), addbaReq->getStartingSequenceNumber(), addbaReq->getBufferSize(), addbaReq->getBlockAckTimeoutValue()); - blockAckAgreements[id] = agreement; - EV_DETAIL << "Block Ack Agreement is added with the following parameters: " << *agreement << endl; - return agreement; - } - else - // TODO update? - return it->second; -} - // // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the peer STA with the Reason Code // field set to TIMEOUT and shall issue a MLME-DELBA.indication primitive with the ReasonCode @@ -102,12 +79,14 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } -const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted) { auto addbaResponse = makeShared(); addbaResponse->setReceiverAddress(addbaRequest->getTransmitterAddress()); - // IEEE Std 802.11-2024, 9.6.4.2 and 11.5.2.3: echo the request identity. + // IEEE Std 802.11-2024, 9.6.4.2 and 11.5.2.3: the response copies the + // request's Dialog Token and reports whether the agreement was accepted. addbaResponse->setDialogToken(addbaRequest->getDialogToken()); + addbaResponse->setStatusCode(accepted ? 0 : 1); // 1: REFUSED_REASON_UNSPECIFIED // The Block Ack Policy subfield is set to 1 for immediate Block Ack and 0 for delayed Block Ack. Tid tid = addbaRequest->getTid(); addbaResponse->setTid(tid); @@ -118,27 +97,17 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR return addbaResponse; } -void RecipientBlockAckAgreementHandler::updateAgreement(const Ptr& addbaResponse) -{ - auto id = std::make_pair(addbaResponse->getReceiverAddress(), addbaResponse->getTid()); - auto it = blockAckAgreements.find(id); - if (it != blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = it->second; - agreement->addbaResposneSent(); - } - else - throw cRuntimeError("Agreement is not found"); -} - -void RecipientBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::removeAgreement(MacAddress originatorAddr, Tid tid) { auto agreementId = std::make_pair(originatorAddr, tid); + lastAddbaResponses.erase(agreementId); auto it = blockAckAgreements.find(agreementId); if (it != blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = it->second; + auto agreement = it->second; blockAckAgreements.erase(it); - delete agreement; + return agreement; } + return nullptr; } RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid tid, MacAddress originatorAddr) @@ -148,35 +117,69 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } -void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { - updateAgreement(addbaResp); - scheduleInactivityTimer(callback); + EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; + bool accepted = addbaRequest->getDialogToken() != 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest); + EV_DETAIL << "Building Addba Response" << endl; + auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy, accepted); + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + bool hadAgreement = blockAckAgreements.find(id) != blockAckAgreements.end(); + // Keep the immutable response body that corresponds to the most recently + // processed request identity. This is response replay state, not a second + // duplicate detector; RecipientQosMacDataService remains authoritative. + if (accepted || hadAgreement) + lastAddbaResponses[id] = addbaResponse; + else + lastAddbaResponses.erase(id); + auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); + RecipientBlockAckAgreement *agreement = nullptr; + if (accepted) { + // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.3: accepting the + // request establishes or modifies the recipient agreement when the + // successful response is formed; transmission is not a state gate. + agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue()); + auto it = blockAckAgreements.find(id); + if (it != blockAckAgreements.end()) { + delete it->second; + it->second = agreement; + } + else + blockAckAgreements[id] = agreement; + scheduleInactivityTimer(agreementHandlerCallback); + } + procedureCallback->processMgmtFrame(addbaResponsePacket, addbaResponse); + return agreement; } -void RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) { - EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest)) { - EV_DETAIL << "Addba Request has been accepted. Creating a new Block Ack Agreement." << endl; - auto agreement = addAgreement(addbaRequest); - EV_DETAIL << "Agreement is added with the following parameters: " << *agreement << endl; - EV_DETAIL << "Building Addba Response" << endl; - auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy); - auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); - callback->processMgmtFrame(addbaResponsePacket, addbaResponse); + auto agreement = getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.3.2.14.3 normally discards duplicate management bodies. + // Replaying the already generated response is an explicit robustness/model + // extension; it does not modify the agreement, reorder window, or inactivity timer. + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + auto it = lastAddbaResponses.find(id); + if (it == lastAddbaResponses.end()) + return; + // Copy the immutable snapshot so outbound sequence assignment uses COW + // and cannot modify the cached body used by a later retransmission. + auto addbaResponse = staticPtrCast(it->second->dupShared()); + procedureCallback->processMgmtFrame(new Packet("AddbaResponse", addbaResponse), addbaResponse); } } -void RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } -void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + return std::unique_ptr(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + return nullptr; } RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 1e65a1557ef..f635b973868 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -26,23 +26,22 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre { protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; + std::map, Ptr> lastAddbaResponses; protected: - virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); - virtual RecipientBlockAckAgreement *addAgreement(const Ptr& addbaReq); - virtual void updateAgreement(const Ptr& addbaResponse); - virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy); + virtual RecipientBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); + virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); public: virtual ~RecipientBlockAckAgreementHandler(); - virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; - virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; + virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processTransmittedDelba(const Ptr& delba) override; + virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 47837ead38a..37967de00f0 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -185,18 +185,17 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } -void BlockAckReordering::processReceivedDelba(const Ptr& delba) +std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress originatorAddr) { - Tid tid = delba->getTid(); - MacAddress originatorAddr = delba->getTransmitterAddress(); + std::vector frames; auto id = std::make_pair(tid, originatorAddr); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { + frames = it->second->extractFrames(); delete it->second; receiveBuffers.erase(it); } - else - EV_DETAIL << "Receive buffer is not found" << endl; + return frames; } void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index a50606f8a53..09cf98a8d4b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -43,7 +43,7 @@ class INET_API BlockAckReordering public: virtual ~BlockAckReordering(); - void processReceivedDelba(const Ptr& delba); + std::vector resetReceiveBuffer(Tid tid, MacAddress originatorAddr); ReorderBuffer processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 1dd9c959c8f..4ab9a0676cb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -77,6 +77,16 @@ void ReceiveBuffer::removeFrame(SequenceNumberCyclic sequenceNumber) throw cRuntimeError("Unknown sequence number: %d", sequenceNumber.get()); } +ReceiveBuffer::Fragments ReceiveBuffer::extractFrames() +{ + Fragments frames; + for (auto& [sequenceNumber, fragments] : buffer) + frames.insert(frames.end(), fragments.begin(), fragments.end()); + buffer.clear(); + length = 0; + return frames; +} + ReceiveBuffer::~ReceiveBuffer() { for (auto fragments : buffer) { diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index a499465d198..70a28e133c2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -37,6 +37,7 @@ class INET_API ReceiveBuffer bool insertFrame(Packet *dataPacket, const Ptr& dataHeader); void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); + Fragments extractFrames(); const ReorderBuffer& getBuffer() { return buffer; } int getLength() { return length; } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 357745c3ddb..875d6f0c557 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -8,6 +8,8 @@ #ifndef __INET_IRECIPIENTBLOCKACKAGREEMENTHANDLER_H #define __INET_IRECIPIENTBLOCKACKAGREEMENTHANDLER_H +#include + #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" @@ -24,10 +26,10 @@ class INET_API IRecipientBlockAckAgreementHandler public: virtual ~IRecipientBlockAckAgreementHandler() {} - virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; - virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual void processTransmittedDelba(const Ptr& delba) = 0; + virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; + virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 8a3ff4c0b7c..4ddd98ad97e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -18,6 +18,14 @@ namespace ieee80211 { class INET_API IRecipientQosMacDataService { public: + // A duplicate has already been consumed and must not be processed as a + // management body; the coordination function may apply a subtype-specific + // response rule using the duplicate flag. + struct ManagementFrameReceptionResult { + std::vector completeFrames; + bool duplicate = false; + }; + static simsignal_t packetDefragmentedSignal; static simsignal_t packetDeaggregatedSignal; @@ -26,7 +34,8 @@ class INET_API IRecipientQosMacDataService virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; - virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddr) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b97f73e7a61..11338db6692 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -32,6 +32,7 @@ using namespace inet::physicallayer; simsignal_t Hcf::edcaCollisionDetectedSignal = cComponent::registerSignal("edcaCollisionDetected"); simsignal_t Hcf::blockAckAgreementAddedSignal = cComponent::registerSignal("blockAckAgreementAdded"); simsignal_t Hcf::blockAckAgreementDeletedSignal = cComponent::registerSignal("blockAckAgreementDeleted"); +simsignal_t Hcf::blockAckAgreementChangedSignal = cComponent::registerSignal("blockAckAgreementChanged"); Define_Module(Hcf); @@ -529,8 +530,9 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const PtrdataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { - sendUp(recipientDataService->managementFrameReceived(packet, mgmtHeader)); - recipientProcessReceivedManagementFrame(mgmtHeader); + auto receptionResult = recipientDataService->managementFrameReceived(packet, mgmtHeader); + sendUp(receptionResult.completeFrames); + recipientProcessReceivedManagementFrame(mgmtHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -553,13 +555,27 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header) +void Hcf::recipientProcessReceivedManagementFrame(const Ptr& header, bool duplicate) { + if (duplicate) { + if (recipientBlockAckAgreementHandler) { + if (auto addbaRequest = dynamicPtrCast(header)) + recipientBlockAckAgreementHandler->processDuplicateAddbaRequest(addbaRequest, this); + } + return; + } if (recipientBlockAckAgreementHandler && originatorBlockAckAgreementHandler) { if (auto addbaRequest = dynamicPtrCast(header)) { - recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this); - auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); - emit(blockAckAgreementAddedSignal, agreement); + bool hadAgreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()) != nullptr; + auto agreement = recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this, this); + if (agreement != nullptr) { + if (hadAgreement) { + recipientDataService->resetBlockAckReordering(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + emit(blockAckAgreementChangedSignal, agreement); + } + else + emit(blockAckAgreementAddedSignal, agreement); + } } else if (auto addbaResp = dynamicPtrCast(header)) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid()); @@ -582,10 +598,14 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { + // IEEE Std 802.11-2024, 9.4.1.16, 10.25.4, and 11.5.3.3: + // Initiator selects the agreement direction; the transmitter is the peer. if (delba->getInitiator()) { - auto agreement = recipientBlockAckAgreementHandler->getAgreement(delba->getTid(), delba->getReceiverAddress()); - emit(blockAckAgreementDeletedSignal, agreement); - recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + if (agreement != nullptr) { + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); + emit(blockAckAgreementDeletedSignal, agreement.get()); + } } else { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); @@ -715,8 +735,8 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } - else if (auto addbaResp = dynamicPtrCast(mgmtHeader)) - recipientBlockAckAgreementHandler->processTransmittedAddbaResp(addbaResp, this); + else if (dynamicPtrCast(mgmtHeader)) + ; // Recipient agreement was established when the successful response was formed. else if (auto delba = dynamicPtrCast(mgmtHeader)) { if (delba->getInitiator()) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); @@ -726,8 +746,16 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) emit(blockAckAgreementDeletedSignal, agreement.get()); } - else - recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + else { + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient + // resources are released whether the recipient transmitted or + // received DELBA. The reorder window is such a resource. + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); + emit(blockAckAgreementDeletedSignal, agreement.get()); + } + } } else ; // TODO other mgmt frames if needed } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index f475aa8a1de..a24eb16ba42 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -32,7 +32,6 @@ #include "inet/linklayer/ieee80211/mac/contract/ITx.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h" -#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" #include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" @@ -55,6 +54,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: static simsignal_t edcaCollisionDetectedSignal; static simsignal_t blockAckAgreementAddedSignal; static simsignal_t blockAckAgreementDeletedSignal; + static simsignal_t blockAckAgreementChangedSignal; protected: Ieee80211Mac *mac = nullptr; @@ -133,6 +133,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual bool processDroppedBlockAckSetupFrame(Packet *packet); virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); virtual void untrackPendingFrame(const Packet *packet); virtual void rebuildPendingFrameEligibility(); @@ -141,7 +143,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Recipient virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header); virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); - virtual void recipientProcessReceivedManagementFrame(const Ptr& header); + virtual void recipientProcessReceivedManagementFrame(const Ptr& header, bool duplicate); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); // Originator @@ -176,8 +178,6 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; - virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; - virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index cac8079b706..cd8113cc9b0 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -55,6 +55,7 @@ module Hcf extends Module like IHcf @signal[datarateSelected](type=double); @signal[blockAckAgreementAdded]; @signal[blockAckAgreementDeleted]; + @signal[blockAckAgreementChanged]; @statistic[packetSentToPeer](title="packets sent"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerUnicast](title="packets sent: unicast"; source=ieee80211Unicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerMulticast](title="packets sent: multicast"; source=ieee80211Multicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @@ -80,6 +81,7 @@ module Hcf extends Module like IHcf @statistic[datarateSelected](title="datarates selected"; record=vector; interpolationmode=none); @statistic[blockAckAgreementAdded](title="added block ack agreements"; record=count); @statistic[blockAckAgreementDeleted](title="deleted block ack agreements"; record=count); + @statistic[blockAckAgreementChanged](title="changed block ack agreements"; record=count); @statistic[blockAckAgreementActive](title="active block ack agreements"; source=warmup(count(blockAckAgreementAdded)-count(blockAckAgreementDeleted)); record=vector; interpolationmode=sample-hold; autoWarmupFilter=false); submodules: diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 77e677bb147..d6d6302d70a 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -30,6 +30,21 @@ void RecipientQosMacDataService::initialize() blockAckReordering = new BlockAckReordering(); } +void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) +{ + Enter_Method("resetBlockAckReordering"); + if (blockAckReordering) { + auto droppedFrames = blockAckReordering->resetReceiveBuffer(tid, originatorAddr); + for (auto packet : droppedFrames) { + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } +} + Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { for (auto fragment : completeFragments) { @@ -112,25 +127,25 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data return deaggregatedFrames; } -std::vector RecipientQosMacDataService::managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) +IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataService::managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) { Enter_Method("managementFrameReceived"); take(mgmtPacket); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption - if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) - return std::vector(); + if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { + delete mgmtPacket; + return { {}, true }; + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting if (dynamicPtrCast(mgmtHeader)) { delete mgmtPacket; - return std::vector(); + return { {}, false }; } else - return std::vector({ mgmtPacket }); + return { { mgmtPacket }, false }; } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index e4b8ee28865..1bc573e4788 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -48,7 +48,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, public: virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; - virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddr) override; }; } /* namespace ieee80211 */ diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 96772f046b9..49932b3bda9 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -659,6 +659,7 @@ /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 33e5-c380/tplx;78d2-bb9a/~tNl;1524-d88f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 7f3b-72ca/tplx;7c57-849b/~tNl;06e3-7901/~tND;3d84-93de/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 5570-9d59/tplx;c521-a55d/~tNl;e5fc-a44c/~tND;e59c-a8ed/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, 4221-b2c3/tplx;f9c2-e9ef/~tNl;5ab0-68ce/~tND, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index ea5449df999..9e3a5a153a3 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -16,17 +16,23 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/queueing/gate/PacketGate.h" @@ -134,6 +140,22 @@ class TestTimeoutCancellationCallback : public TestCallback } }; +class TestRecipientHandler : public RecipientBlockAckAgreementHandler +{ + public: + int getNumCachedAddbaResponses() const { return lastAddbaResponses.size(); } +}; + +class TestBlockAckReordering : public BlockAckReordering +{ + public: + int getNumReceiveBuffers() const { return receiveBuffers.size(); } + ReceiveBuffer *getReceiveBuffer(Tid tid, MacAddress originatorAddress) const { + auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; + class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue { public: @@ -204,6 +226,17 @@ class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } }; +class TestImmediateRemovalQueue : public TestPacketQueue +{ + public: + PacketRemovalReason removalReason = PacketRemovalReason::DROPPED; + + virtual void enqueuePacket(Packet *packet) override { + notifyPacketRemoved(packet, removalReason); + delete packet; + } +}; + class TestWrongDequeueQueue : public TestPacketQueue { public: @@ -380,6 +413,14 @@ class TestPacketPulledListener : public cListener } }; +class TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + class TestPacketArrivalListener : public cListener { public: @@ -392,12 +433,12 @@ class TestPacketArrivalListener : public cListener } }; -class TestSignalListener : public cListener +class TestOrderedSignalListener : public cListener { public: - int numSignals = 0; + std::vector signals; - virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } + virtual void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { signals.push_back(signal); } }; class TestPacketDropSignalListener : public cListener @@ -499,6 +540,23 @@ class TestEdca : public Edca virtual NonQosRecoveryProcedure *getMgmtAndNonQoSRecoveryProcedure() const override { return nonQosRecoveryProcedure; } }; +class TestRecipientDataService : public IRecipientQosMacDataService +{ + public: + int numReorderingResets = 0; + Tid lastTid = -1; + MacAddress lastOriginatorAddress; + + virtual std::vector dataFrameReceived(Packet *, const Ptr&, IRecipientBlockAckAgreementHandler *) override { return {}; } + virtual std::vector controlFrameReceived(Packet *, const Ptr&, IRecipientBlockAckAgreementHandler *) override { return {}; } + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *, const Ptr&) override { return {}; } + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddress) override { + numReorderingResets++; + lastTid = tid; + lastOriginatorAddress = originatorAddress; + } +}; + class TestHcf : public Hcf { public: @@ -557,6 +615,7 @@ class TestHcf : public Hcf void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { @@ -580,6 +639,14 @@ class TestHcf : public Hcf virtual void scheduleAddbaResponseTimer(simtime_t) override {} }; +class TestTxopProcedure : public TxopProcedure +{ + public: + std::vector *eventOrder = nullptr; + virtual simtime_t getRemaining() const override { return 1; } + virtual void endTxop() override { if (eventOrder != nullptr) eventOrder->push_back(2); } +}; + class TestMac : public Ieee80211Mac { public: @@ -588,6 +655,15 @@ class TestMac : public Ieee80211Mac virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } }; +class TestEmptyFrameSequence : public IFrameSequence +{ + public: + virtual void startSequence(FrameSequenceContext *, int) override {} + virtual IFrameSequenceStep *prepareStep(FrameSequenceContext *) override { return nullptr; } + virtual bool completeStep(FrameSequenceContext *) override { return true; } + virtual std::string getHistory() const override { return ""; } +}; + class TestFragmentationPolicy : public IFragmentationPolicy { public: @@ -715,6 +791,38 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler virtual void handleStartRxTimeout() override {} }; +class TestDcaf : public Dcaf +{ + public: + int numReleasedChannels = 0; + + void configure(InProgressFrames *inProgressFrames) { this->inProgressFrames = inProgressFrames; } + virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleasedChannels++; } +}; + +class TestDcf : public Dcf +{ + public: + virtual ~TestDcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac) { + this->channelAccess = channelAccess; + this->mac = mac; + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); + auto handler = new TestFrameSequenceHandler(); + frameSequenceHandler = handler; + return handler; + } + void grant(TestDcaf *channelAccess) { channelGranted(channelAccess); } + + protected: + virtual FrameSequenceContext *buildContext() override { + return new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, channelAccess->getInProgressFrames(), nullptr, nullptr, new NonQoSContext(nullptr), nullptr); + } +}; + static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) { auto header = makeShared(); @@ -751,6 +859,18 @@ static Ptr makeResponse(MacAddress transmitterAddress, T return response; } +static Ptr makeRequest(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, SequenceNumberCyclic startingSequenceNumber, int bufferSize) +{ + auto request = makeShared(); + request->setTransmitterAddress(transmitterAddress); + request->setTid(tid); + request->setDialogToken(dialogToken); + request->setStartingSequenceNumber(startingSequenceNumber); + request->setBufferSize(bufferSize); + request->setBlockAckPolicy(1); + request->setBlockAckTimeoutValue(0); + return request; +} %activity: // IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.2. @@ -1443,6 +1563,1018 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(threw); } +// An initially rejected request has no agreement, so its response is never +// replayable and must not consume replay-cache state. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("initiallyRejectedAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy rejectingPolicy; + rejectingPolicy.accepted = false; + TestCallback responseCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &rejectingPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&responseCallback); + + auto rejectedRequest = makeRequest(peer3, 7, 89, SequenceNumberCyclic(90), 16); + rejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); + auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("initiallyRejectedAddbaRequest", rejectedRequest), rejectedRequest); + ASSERT(!rejectedReception.duplicate); + hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(7, peer3) == nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + ASSERT(dynamicPtrCast(responseCallback.managementHeader)->getStatusCode() != 0); + + auto duplicateRejectedRequest = makeRequest(peer3, 7, 89, SequenceNumberCyclic(90), 16); + duplicateRejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); + duplicateRejectedRequest->setRetry(true); + auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateInitiallyRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); + ASSERT(duplicateReception.duplicate); + hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateReception.duplicate); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// IEEE Std 802.11-2024, 9.2.4.1.6 and 10.3.2.14.3/Table 10-6: +// duplicate identity comes from Retry + TA + MAC sequence/fragment, not from +// equal ADDBA parameters. Re-sending the cached exact response is an +// explicit robustness/model extension; it leaves recipient state untouched. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("duplicateAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener reorderDropListener; + recipientDataService->subscribe(packetDroppedSignal, &reorderDropListener); + + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy duplicatePolicy; + duplicatePolicy.blockAckTimeoutValue = 1; + TestCallback responseCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &duplicatePolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&responseCallback); + TestSignalListener addedListener; + TestSignalListener changedListener; + hcf.subscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + hcf.subscribe(Hcf::blockAckAgreementChangedSignal, &changedListener); + + auto firstRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + firstRequest->setSequenceNumber(SequenceNumberCyclic(50)); + firstRequest->setBlockAckTimeoutValue(3); + auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); + ASSERT(!firstReception.duplicate); + ASSERT(firstReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(firstRequest, firstReception.duplicate); + auto firstAgreement = recipientHandler->getAgreement(6, peer2); + ASSERT(firstAgreement != nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + auto firstBlockAckRecord = firstAgreement->getBlockAckRecord(); + auto firstExpirationTime = firstAgreement->getExpirationTime(); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + auto firstResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(firstResponse != nullptr); + ASSERT(firstResponse->getDialogToken() == 90); + ASSERT(firstResponse->getStatusCode() == 0); + ASSERT(firstResponse->getTid() == 6); + ASSERT(firstResponse->getBlockAckPolicy()); + ASSERT(firstResponse->getBufferSize() == 32); + ASSERT(firstResponse->getBlockAckTimeoutValue() == 3); + ASSERT(firstResponse->getAMsduSupported()); + + auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(101)); + bufferedHeader->setTransmitterAddress(peer2); + auto bufferedPacket = new Packet("duplicateAddbaBufferedData", bufferedHeader); + ASSERT(recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, recipientHandler).empty()); + + // Mutable policy is deliberately changed after negotiation. A true MAC + // duplicate must replay the original successful response, not rebuild it. + duplicatePolicy.accepted = false; + duplicatePolicy.blockAckTimeoutValue = 0; + duplicatePolicy.aMsduSupportedValue = false; + duplicatePolicy.maximumAllowedBufferSize = 1; + auto duplicateRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + duplicateRequest->setSequenceNumber(SequenceNumberCyclic(50)); + duplicateRequest->setRetry(true); + duplicateRequest->setBlockAckTimeoutValue(3); + auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateAddbaRequest", duplicateRequest), duplicateRequest); + ASSERT(duplicateReception.duplicate); + ASSERT(duplicateReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(duplicateRequest, duplicateReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(firstAgreement->getBlockAckRecord() == firstBlockAckRecord); + ASSERT(firstAgreement->getExpirationTime() == firstExpirationTime); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 2); + auto duplicateResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(duplicateResponse->getDialogToken() == firstResponse->getDialogToken()); + ASSERT(duplicateResponse->getStatusCode() == firstResponse->getStatusCode()); + ASSERT(duplicateResponse->getTid() == firstResponse->getTid()); + ASSERT(duplicateResponse->getBlockAckPolicy() == firstResponse->getBlockAckPolicy()); + ASSERT(duplicateResponse->getBufferSize() == firstResponse->getBufferSize()); + ASSERT(duplicateResponse->getBlockAckTimeoutValue() == firstResponse->getBlockAckTimeoutValue()); + ASSERT(duplicateResponse->getAMsduSupported() == firstResponse->getAMsduSupported()); + + // A rejected fresh renegotiation leaves agreement A in place, but its + // retransmission must replay B's rejection rather than A's old success. + duplicatePolicy.blockAckTimeoutValue = 1; + auto rejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); + rejectedRequest->setSequenceNumber(SequenceNumberCyclic(51)); + rejectedRequest->setBlockAckTimeoutValue(4); + auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("rejectedAddbaRequest", rejectedRequest), rejectedRequest); + ASSERT(!rejectedReception.duplicate); + hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 3); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + auto rejectedResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(rejectedResponse->getDialogToken() == 91); + ASSERT(rejectedResponse->getStatusCode() != 0); + ASSERT(rejectedResponse->getTid() == 6); + ASSERT(rejectedResponse->getBufferSize() == 1); + ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 4); + ASSERT(!rejectedResponse->getAMsduSupported()); + + auto duplicateRejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); + duplicateRejectedRequest->setSequenceNumber(SequenceNumberCyclic(51)); + duplicateRejectedRequest->setRetry(true); + duplicateRejectedRequest->setBlockAckTimeoutValue(4); + auto duplicateRejectedReception = recipientDataService->managementFrameReceived(new Packet("duplicateRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); + ASSERT(duplicateRejectedReception.duplicate); + hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateRejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 4); + auto duplicateRejectedResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(duplicateRejectedResponse->getDialogToken() == rejectedResponse->getDialogToken()); + ASSERT(duplicateRejectedResponse->getStatusCode() == rejectedResponse->getStatusCode()); + ASSERT(duplicateRejectedResponse->getTid() == rejectedResponse->getTid()); + ASSERT(duplicateRejectedResponse->getBlockAckPolicy() == rejectedResponse->getBlockAckPolicy()); + ASSERT(duplicateRejectedResponse->getBufferSize() == rejectedResponse->getBufferSize()); + ASSERT(duplicateRejectedResponse->getBlockAckTimeoutValue() == rejectedResponse->getBlockAckTimeoutValue()); + ASSERT(duplicateRejectedResponse->getAMsduSupported() == rejectedResponse->getAMsduSupported()); + + auto expectedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + expectedHeader->setTransmitterAddress(peer2); + auto expectedPacket = new Packet("duplicateAddbaExpectedData", expectedHeader); + auto expectedFrames = recipientDataService->dataFrameReceived(expectedPacket, expectedHeader, recipientHandler); + ASSERT(expectedFrames.size() == 1); + ASSERT(expectedFrames.front() == expectedPacket); + auto followingHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(102)); + followingHeader->setTransmitterAddress(peer2); + auto followingPacket = new Packet("duplicateAddbaFollowingData", followingHeader); + auto followingFrames = recipientDataService->dataFrameReceived(followingPacket, followingHeader, recipientHandler); + ASSERT(followingFrames.size() == 1); + ASSERT(followingFrames.front() == bufferedPacket); + for (auto packet : expectedFrames) { + take(packet); + drop(packet); + delete packet; + } + for (auto packet : followingFrames) { + take(packet); + drop(packet); + delete packet; + } + + // A new MAC identity with the exact same ADDBA body is a genuine 10.25.2 + // renegotiation: replace the agreement and reset the reorder window, + // which still contains followingPacket (sequence number 102). + duplicatePolicy.accepted = true; + duplicatePolicy.blockAckTimeoutValue = 1; + duplicatePolicy.aMsduSupportedValue = true; + duplicatePolicy.maximumAllowedBufferSize = 64; + auto replacementRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + replacementRequest->setSequenceNumber(SequenceNumberCyclic(52)); + replacementRequest->setBlockAckTimeoutValue(3); + auto replacementReception = recipientDataService->managementFrameReceived(new Packet("replacementAddbaRequest", replacementRequest), replacementRequest); + ASSERT(!replacementReception.duplicate); + ASSERT(replacementReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(replacementRequest, replacementReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) != firstAgreement); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 1); + ASSERT(reorderDropListener.numSignals == 1); + ASSERT(reorderDropListener.numOtherPacketDrops == 1); + ASSERT(responseCallback.managementPackets.size() == 5); + + auto duplicateWithoutAgreement = makeRequest(peer3, 7, 91, SequenceNumberCyclic(200), 16); + hcf.processReceivedManagementFrame(duplicateWithoutAgreement, true); + ASSERT(responseCallback.managementPackets.size() == 5); + + auto duplicateDelba = makeShared(); + duplicateDelba->setTransmitterAddress(peer2); + duplicateDelba->setTid(6); + duplicateDelba->setInitiator(true); + auto replacementAgreement = recipientHandler->getAgreement(6, peer2); + hcf.processReceivedManagementFrame(duplicateDelba, true); + ASSERT(recipientHandler->getAgreement(6, peer2) == replacementAgreement); + + hcf.processReceivedManagementFrame(duplicateDelba, false); + ASSERT(recipientHandler->getAgreement(6, peer2) == nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + auto duplicateAfterTeardown = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + duplicateAfterTeardown->setSequenceNumber(SequenceNumberCyclic(52)); + duplicateAfterTeardown->setRetry(true); + hcf.processReceivedManagementFrame(duplicateAfterTeardown, true); + ASSERT(responseCallback.managementPackets.size() == 5); + + hcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + hcf.unsubscribe(Hcf::blockAckAgreementChangedSignal, &changedListener); + recipientDataService->unsubscribe(packetDroppedSignal, &reorderDropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. +RecipientBlockAckAgreementHandler immediateRecipientHandler; +TestRecipientPolicy recipientPolicy; +TestCallback recipientCallback; +auto recipientRequest = makeShared(); +recipientRequest->setTransmitterAddress(peer1); +recipientRequest->setTid(6); +recipientRequest->setDialogToken(77); +recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(0)); +recipientRequest->setBufferSize(64); +recipientRequest->setBlockAckPolicy(1); +recipientRequest->setBlockAckTimeoutValue(0); +auto firstImmediateAgreement = immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback); +ASSERT(firstImmediateAgreement != nullptr); +ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == firstImmediateAgreement); +ASSERT(firstImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); +ASSERT(acceptedResponse->getDialogToken() == 77); +ASSERT(acceptedResponse->getStatusCode() == 0); + +// Renegotiation replaces the recipient agreement and the HCF reset boundary +// must discard the old reorder window before frames use the new SSN. +RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; +{ + TestBlockAckReordering reordering; + auto oldWindowHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(1)); + auto oldWindowPacket = new Packet("oldWindowPacket", oldWindowHeader); + auto oldWindowFrames = reordering.processReceivedQoSFrame(firstImmediateAgreement, oldWindowPacket, oldWindowHeader); + ASSERT(oldWindowFrames.empty()); + ASSERT(reordering.getNumReceiveBuffers() == 1); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getLength() == 1); + + auto discardedOldWindowFrames = reordering.resetReceiveBuffer(6, peer1); + ASSERT(discardedOldWindowFrames.size() == 1); + ASSERT(discardedOldWindowFrames.front() == oldWindowPacket); + take(discardedOldWindowFrames.front()); + drop(discardedOldWindowFrames.front()); + delete discardedOldWindowFrames.front(); + ASSERT(reordering.getNumReceiveBuffers() == 0); + + recipientRequest->setDialogToken(79); + recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(9)); + replacementImmediateAgreement = immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback); + ASSERT(replacementImmediateAgreement != nullptr); + ASSERT(replacementImmediateAgreement != firstImmediateAgreement); + ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateAgreement); + ASSERT(replacementImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(9)); + auto newWindowHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(10)); + auto newWindowPacket = new Packet("newWindowPacket", newWindowHeader); + auto newWindowFrames = reordering.processReceivedQoSFrame(replacementImmediateAgreement, newWindowPacket, newWindowHeader); + ASSERT(newWindowFrames.empty()); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getNextExpectedSequenceNumber() == SequenceNumberCyclic(9)); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getLength() == 1); + auto discardedNewWindowFrames = reordering.resetReceiveBuffer(6, peer1); + ASSERT(discardedNewWindowFrames.size() == 1); + ASSERT(discardedNewWindowFrames.front() == newWindowPacket); + take(discardedNewWindowFrames.front()); + drop(discardedNewWindowFrames.front()); + delete discardedNewWindowFrames.front(); +} + +// The recipient data service owns reorder-buffer reset observability: every +// buffered MPDU is reported exactly once before it is deleted. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("reorderingDropDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener dropListener; + recipientDataService->subscribe(packetDroppedSignal, &dropListener); + auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(11)); + bufferedHeader->setTransmitterAddress(peer1); + auto bufferedPacket = new Packet("signaledOldWindowPacket", bufferedHeader); + auto signaledOldWindowFrames = recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, &immediateRecipientHandler); + ASSERT(signaledOldWindowFrames.empty()); + recipientDataService->resetBlockAckReordering(6, peer1); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + recipientDataService->resetBlockAckReordering(6, peer1); + ASSERT(dropListener.numSignals == 1); + recipientDataService->unsubscribe(packetDroppedSignal, &dropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +recipientPolicy.accepted = false; +recipientRequest->setDialogToken(80); +recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(12)); +ASSERT(immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback) == nullptr); +ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateAgreement); + +// A received DELBA is keyed by its transmitter (the peer), not by its receiver +// address (the local station). +{ + RecipientBlockAckAgreementHandler delbaRecipientHandler; + TestRecipientPolicy delbaRecipientPolicy; + TestCallback delbaRecipientCallback; + auto request = makeRequest(peer2, 4, 81, SequenceNumberCyclic(12), 32); + auto establishedAgreement = delbaRecipientHandler.processReceivedAddbaRequest(request, &delbaRecipientPolicy, &delbaRecipientCallback, &delbaRecipientCallback); + ASSERT(establishedAgreement != nullptr); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(4); + delba->setInitiator(true); + auto terminatedAgreement = delbaRecipientHandler.processReceivedDelba(delba, &delbaRecipientPolicy); + ASSERT(terminatedAgreement != nullptr); + ASSERT(delbaRecipientHandler.getAgreement(4, peer2) == nullptr); +} + +// HCF emits one deletion event for the peer-keyed recipient agreement and can +// then be torn down without walking an EDCA child hierarchy. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestCallback delbaCallback; + auto request = makeRequest(peer2, 2, 82, SequenceNumberCyclic(14), 32); + recipientPolicy.accepted = true; + auto establishedAgreement = recipientHandler->processReceivedAddbaRequest(request, &recipientPolicy, &delbaCallback, &delbaCallback); + ASSERT(establishedAgreement != nullptr); + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + TestRecipientDataService recipientDataService; + hcf.configureRecipientDataService(&recipientDataService); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(2); + delba->setInitiator(true); + hcf.processReceivedManagementFrame(delba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(recipientDataService.lastTid == 2); + ASSERT(recipientDataService.lastOriginatorAddress == peer2); + ASSERT(recipientHandler->getAgreement(2, peer2) == nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// A rejected DELBA leaves the recipient agreement, reordering state, and +// agreement statistics unchanged. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy rejectingPolicy; + TestCallback delbaCallback; + auto request = makeRequest(peer2, 2, 83, SequenceNumberCyclic(15), 32); + auto establishedAgreement = recipientHandler->processReceivedAddbaRequest(request, &rejectingPolicy, &delbaCallback, &delbaCallback); + ASSERT(establishedAgreement != nullptr); + rejectingPolicy.delbaAccepted = false; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &rejectingPolicy); + TestRecipientDataService recipientDataService; + hcf.configureRecipientDataService(&recipientDataService); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(2); + delba->setInitiator(true); + hcf.processReceivedManagementFrame(delba); + ASSERT(deletionListener.numSignals == 0); + ASSERT(recipientDataService.numReorderingResets == 0); + ASSERT(recipientHandler->getAgreement(2, peer2) != nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: locally transmitting a +// recipient DELBA tears down the agreement and its reorder resources before a +// replacement agreement for the same peer and TID starts a fresh window. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("transmittedDelbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener reorderDropListener; + recipientDataService->subscribe(packetDroppedSignal, &reorderDropListener); + + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy transmittedDelbaPolicy; + TestCallback transmittedDelbaCallback; + auto firstRequest = makeRequest(peer2, 3, 84, SequenceNumberCyclic(100), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(firstRequest, &transmittedDelbaPolicy, &transmittedDelbaCallback, &transmittedDelbaCallback); + ASSERT(firstAgreement != nullptr); + auto oldWindowHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(101)); + oldWindowHeader->setTransmitterAddress(peer2); + auto oldWindowPacket = new Packet("transmittedDelbaOldWindow", oldWindowHeader); + ASSERT(recipientDataService->dataFrameReceived(oldWindowPacket, oldWindowHeader, recipientHandler).empty()); + + QosAckHandler transmittedDelbaAckHandler; + TestEdcaf transmittedDelbaEdcaf; + transmittedDelbaEdcaf.qosAckHandler = &transmittedDelbaAckHandler; + TestEdca transmittedDelbaEdca; + transmittedDelbaEdca.edcaf = &transmittedDelbaEdcaf; + OriginatorQosAckPolicy transmittedDelbaAckPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &transmittedDelbaPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureTransmittedManagement(&transmittedDelbaEdca, &transmittedDelbaAckPolicy); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + + auto delba = makeShared(); + delba->setReceiverAddress(peer2); + delba->setTid(3); + delba->setInitiator(false); + delba->setSequenceNumber(SequenceNumberCyclic(200)); + Packet delbaPacket("transmittedRecipientDelba", delba); + hcf.processTransmittedManagement(&delbaPacket, delba, AC_BE); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(deletionListener.numSignals == 1); + ASSERT(reorderDropListener.numOtherPacketDrops == 1); + + auto replacementRequest = makeRequest(peer2, 3, 85, SequenceNumberCyclic(10), 32); + auto replacementAgreement = recipientHandler->processReceivedAddbaRequest(replacementRequest, &transmittedDelbaPolicy, &transmittedDelbaCallback, &transmittedDelbaCallback); + ASSERT(replacementAgreement != nullptr); + auto newWindowHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(11)); + newWindowHeader->setTransmitterAddress(peer2); + auto newWindowPacket = new Packet("transmittedDelbaNewWindow", newWindowHeader); + ASSERT(recipientDataService->dataFrameReceived(newWindowPacket, newWindowHeader, recipientHandler).empty()); + recipientDataService->resetBlockAckReordering(3, peer2); + ASSERT(reorderDropListener.numOtherPacketDrops == 2); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + recipientDataService->unsubscribe(packetDroppedSignal, &reorderDropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// The reverse DELBA direction terminates the originator agreement and emits +// exactly one deletion event; policy rejection emits nothing and preserves it. +{ + auto originatorHandler = new TestOriginatorHandler(); + originatorHandler->addEstablishedAgreement(peer3, 5); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy delbaPolicy; + TestRecipientPolicy delbaRecipientPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &delbaPolicy, recipientHandler, &delbaRecipientPolicy); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto acceptedDelba = makeShared(); + acceptedDelba->setTransmitterAddress(peer3); + acceptedDelba->setReceiverAddress(peer1); + acceptedDelba->setTid(5); + acceptedDelba->setInitiator(false); + hcf.processReceivedManagementFrame(acceptedDelba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(originatorHandler->getAgreement(peer3, 5) == nullptr); + originatorHandler->addEstablishedAgreement(peer3, 5); + delbaPolicy.delbaAccepted = false; + auto rejectedDelba = makeShared(*acceptedDelba); + hcf.processReceivedManagementFrame(rejectedDelba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(originatorHandler->getAgreement(peer3, 5) != nullptr); + + QosAckHandler transmittedOriginatorAckHandler; + TestEdcaf transmittedOriginatorEdcaf; + transmittedOriginatorEdcaf.qosAckHandler = &transmittedOriginatorAckHandler; + TestPacketQueue transmittedOriginatorPendingQueue; + TestInProgressFrames transmittedOriginatorInProgressFrames; + TestOriginatorQosMacDataService transmittedOriginatorDataService; + transmittedOriginatorInProgressFrames.configure(&transmittedOriginatorDataService, &transmittedOriginatorAckHandler, &transmittedOriginatorPendingQueue); + transmittedOriginatorEdcaf.pendingQueue = &transmittedOriginatorPendingQueue; + transmittedOriginatorEdcaf.inProgressFrames = &transmittedOriginatorInProgressFrames; + TestEdca transmittedOriginatorEdca; + transmittedOriginatorEdca.edcaf = &transmittedOriginatorEdcaf; + OriginatorQosAckPolicy transmittedOriginatorAckPolicy; + hcf.configureTransmittedManagement(&transmittedOriginatorEdca, &transmittedOriginatorAckPolicy); + originatorHandler->addEstablishedAgreement(peer3, 6); + auto transmittedEstablishedDelba = makeShared(); + transmittedEstablishedDelba->setReceiverAddress(peer3); + transmittedEstablishedDelba->setTid(6); + transmittedEstablishedDelba->setInitiator(true); + transmittedEstablishedDelba->setSequenceNumber(SequenceNumberCyclic(201)); + Packet transmittedEstablishedDelbaPacket("transmittedEstablishedOriginatorDelba", transmittedEstablishedDelba); + hcf.processTransmittedManagement(&transmittedEstablishedDelbaPacket, transmittedEstablishedDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + ASSERT(originatorHandler->getAgreement(peer3, 6) == nullptr); + hcf.processTransmittedManagement(&transmittedEstablishedDelbaPacket, transmittedEstablishedDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + + TestCallback pendingDelbaCallback; + Packet pendingDelbaTrigger("pendingDelbaTrigger"); + auto pendingDelbaTriggerHeader = makeQosHeader(peer3, 7, SequenceNumberCyclic(0)); + originatorHandler->processAcknowledgedDataFrame(&pendingDelbaTrigger, pendingDelbaTriggerHeader, &delbaPolicy, &pendingDelbaCallback); + ASSERT(originatorHandler->isAddbaResponsePending(peer3, 7)); + auto transmittedPendingDelba = makeShared(); + transmittedPendingDelba->setReceiverAddress(peer3); + transmittedPendingDelba->setTid(7); + transmittedPendingDelba->setInitiator(true); + transmittedPendingDelba->setSequenceNumber(SequenceNumberCyclic(202)); + Packet transmittedPendingDelbaPacket("transmittedPendingOriginatorDelba", transmittedPendingDelba); + hcf.processTransmittedManagement(&transmittedPendingDelbaPacket, transmittedPendingDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + ASSERT(originatorHandler->getAgreement(peer3, 7) == nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// IEEE Std 802.11-2024, 11.5.2.2 and 11.5.3.2: a received DELBA can cancel +// an originator transaction before ADDBA Response success; that pending state +// was never observable as an active agreement and must not emit Deleted. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy pendingPolicy; + TestRecipientPolicy pendingRecipientPolicy; + TestOriginatorQosMacDataService pendingDataService; + pendingDataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return originatorHandler->isAddbaRequestPending(packet, addbaReq); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !originatorHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + TestPacketQueue pendingQueue; + QosAckHandler pendingAckHandler; + TestInProgressFrames pendingInProgressFrames; + pendingInProgressFrames.configure(&pendingDataService, &pendingAckHandler, &pendingQueue); + TestEdcaf pendingEdcaf; + pendingEdcaf.pendingQueue = &pendingQueue; + pendingEdcaf.inProgressFrames = &pendingInProgressFrames; + pendingEdcaf.qosAckHandler = &pendingAckHandler; + TestEdca pendingEdca; + pendingEdca.edcaf = &pendingEdcaf; + TestHcf pendingHcf; + pendingHcf.configureBlockAckHandlers(originatorHandler, &pendingPolicy, recipientHandler, &pendingRecipientPolicy); + pendingHcf.configureEligibilityIndex(&pendingEdca, &pendingDataService); + pendingHcf.observePendingQueue(&pendingQueue); + TestCallback pendingCallback; + Packet pendingTrigger("receivedPendingDelbaTrigger"); + auto pendingHeader = makeQosHeader(peer3, 8, SequenceNumberCyclic(203)); + originatorHandler->processAcknowledgedDataFrame(&pendingTrigger, pendingHeader, &pendingPolicy, &pendingCallback); + auto pendingRequestPacket = pendingCallback.managementPacket; + auto pendingRequest = dynamicPtrCast(pendingCallback.managementHeader); + pendingCallback.forgetManagementPacket(pendingRequestPacket); + auto heldPacket = new Packet("receivedPendingDelbaHeld", makeQosHeader(peer3, 8, SequenceNumberCyclic(204))); + pendingQueue.enqueuePacket(pendingRequestPacket); + pendingQueue.enqueuePacket(heldPacket); + pendingHcf.trackFrame(pendingRequestPacket, AC_BK); + pendingHcf.trackFrame(heldPacket, AC_BK); + ASSERT(originatorHandler->isAddbaResponsePending(peer3, 8)); + ASSERT(pendingHcf.getNumEligiblePendingFrames(AC_BK) == 1); + TestSignalListener pendingDeletionListener; + pendingHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &pendingDeletionListener); + auto receivedPendingDelba = makeShared(); + receivedPendingDelba->setTransmitterAddress(peer3); + receivedPendingDelba->setReceiverAddress(peer1); + receivedPendingDelba->setTid(8); + receivedPendingDelba->setInitiator(false); + pendingHcf.processReceivedManagementFrame(receivedPendingDelba); + ASSERT(pendingDeletionListener.numSignals == 0); + ASSERT(originatorHandler->getAgreement(peer3, 8) == nullptr); + ASSERT(pendingHcf.numCancelledAddbaTransactions == 1); + ASSERT(pendingHcf.numRebuildEligibilityCalls > 0); + ASSERT(pendingQueue.getNumPackets() == 1); + ASSERT(pendingQueue.getPacket(0) == heldPacket); + ASSERT(pendingDataService.isFrameEligible(heldPacket)); + ASSERT(pendingHcf.getNumEligiblePendingFrames(AC_BK) == 1); + ASSERT(pendingHcf.numResumedEligibleChannelAccess == 1); + pendingHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &pendingDeletionListener); + pendingQueue.removePacket(heldPacket); + delete heldPacket; +} + +// A locally vetoed successful response immediately restores data eligibility +// even while its best-effort DELBA remains queued. Once retry backoff expires, +// a replacement ADDBA invalidates the old DELBA without requiring a disposal +// callback from the queue. +{ + TestPacketQueue heldTeardownQueue; + auto heldTeardownOriginatorHandler = new TestOriginatorHandler(); + auto heldTeardownRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy heldTeardownOriginatorPolicy; + heldTeardownOriginatorPolicy.addbaAccepted = false; + TestRecipientPolicy heldTeardownRecipientPolicy; + TestOriginatorQosMacDataService heldTeardownDataService; + heldTeardownDataService.setFrameEligibilityFunction([heldTeardownOriginatorHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return heldTeardownOriginatorHandler->isAddbaRequestPending(packet, addbaReq); + if (auto delba = dynamicPtrCast(header)) + return heldTeardownOriginatorHandler->isDelbaPending(packet, delba); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !heldTeardownOriginatorHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + QosAckHandler heldTeardownAckHandler; + TestInProgressFrames heldTeardownInProgressFrames; + heldTeardownInProgressFrames.configure(&heldTeardownDataService, &heldTeardownAckHandler, &heldTeardownQueue); + TestEdcaf heldTeardownEdcaf; + heldTeardownEdcaf.pendingQueue = &heldTeardownQueue; + heldTeardownEdcaf.inProgressFrames = &heldTeardownInProgressFrames; + heldTeardownEdcaf.qosAckHandler = &heldTeardownAckHandler; + TestEdca heldTeardownEdca; + heldTeardownEdca.edcaf = &heldTeardownEdcaf; + TestHcf heldTeardownHcf; + heldTeardownHcf.configureBlockAckHandlers(heldTeardownOriginatorHandler, &heldTeardownOriginatorPolicy, heldTeardownRecipientHandler, &heldTeardownRecipientPolicy); + heldTeardownHcf.configureEligibilityIndex(&heldTeardownEdca, &heldTeardownDataService); + heldTeardownHcf.observePendingQueue(&heldTeardownQueue); + TestCallback heldTeardownCallback; + Packet heldTeardownTrigger("heldTeardownTrigger"); + auto heldTeardownTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(210)); + heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownTrigger, heldTeardownTriggerHeader, &heldTeardownOriginatorPolicy, &heldTeardownCallback); + auto heldTeardownRequest = dynamicPtrCast(heldTeardownCallback.managementHeader); + heldTeardownOriginatorHandler->processTransmittedAddbaReq(heldTeardownCallback.managementPacket, heldTeardownRequest, &heldTeardownOriginatorPolicy, &heldTeardownCallback); + auto heldDataPacket = new Packet("heldTeardownData", makeQosHeader(peer1, 7, SequenceNumberCyclic(211))); + heldTeardownQueue.enqueuePacket(heldDataPacket); + heldTeardownHcf.trackFrame(heldDataPacket, AC_BK); + ASSERT(!heldTeardownDataService.isFrameEligible(heldDataPacket)); + TestOrderedSignalListener transitionListener; + heldTeardownHcf.subscribe(Hcf::blockAckAgreementAddedSignal, &transitionListener); + heldTeardownHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &transitionListener); + auto heldTeardownResponse = makeResponse(peer1, 7, heldTeardownRequest->getDialogToken(), 0); + heldTeardownHcf.processReceivedManagementFrame(heldTeardownResponse); + ASSERT(transitionListener.signals == std::vector({ Hcf::blockAckAgreementAddedSignal, Hcf::blockAckAgreementDeletedSignal })); + ASSERT(heldTeardownOriginatorHandler->getAgreement(peer1, 7) == nullptr); + ASSERT(heldTeardownHcf.numRebuildEligibilityCalls > 0); + ASSERT(heldTeardownDataService.isFrameEligible(heldDataPacket)); + ASSERT(heldTeardownHcf.hasFrame(AC_BK)); + ASSERT(heldTeardownQueue.getNumPackets() == 2); + auto heldDelbaPacket = heldTeardownQueue.getPacket(1); + auto heldDelba = dynamicPtrCast(heldDelbaPacket->peekAtFront()); + ASSERT(heldDelba != nullptr); + ASSERT(heldTeardownOriginatorHandler->isDelbaPending(heldDelbaPacket, heldDelba)); + OriginatorQosAckPolicy heldTeardownAckPolicy; + ASSERT(heldTeardownAckPolicy.computeAckPolicy(heldDataPacket, heldDataPacket->peekAtFront(), heldTeardownOriginatorHandler->getAgreement(peer1, 7)) == NORMAL_ACK); + heldTeardownOriginatorHandler->allowRetryNow(peer1, 7); + TestCallback heldTeardownRetryCallback; + Packet heldTeardownRetryTrigger("heldTeardownRetryTrigger"); + auto heldTeardownRetryHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(212)); + auto obsoleteHeldTeardownTransactionId = heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownRetryTrigger, heldTeardownRetryHeader, &heldTeardownOriginatorPolicy, &heldTeardownRetryCallback); + ASSERT(heldTeardownRetryCallback.managementPacket != nullptr); + ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getTransactionId()); + heldTeardownHcf.cancelTransaction(obsoleteHeldTeardownTransactionId); + ASSERT(heldTeardownQueue.getNumPackets() == 1); + ASSERT(heldTeardownQueue.getPacket(0) == heldDataPacket); + heldTeardownHcf.rebuildEligibilityIndex(); + heldTeardownHcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &transitionListener); + heldTeardownHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &transitionListener); + while (!heldTeardownQueue.isEmpty()) { + auto packet = heldTeardownQueue.getPacket(0); + heldTeardownQueue.removePacket(packet); + delete packet; + } +} + +// Synchronous DELBA drop/removal clears the separate teardown transaction, and +// the already-completed agreement transition remains idempotent. +for (auto removalReason : { queueing::IPacketQueue::PacketRemovalReason::DROPPED, queueing::IPacketQueue::PacketRemovalReason::REMOVED }) { + TestImmediateRemovalQueue teardownQueue; + teardownQueue.removalReason = removalReason; + TestOriginatorQosMacDataService teardownDataService; + teardownDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + QosAckHandler teardownAckHandler; + TestInProgressFrames teardownInProgressFrames; + teardownInProgressFrames.configure(&teardownDataService, &teardownAckHandler, &teardownQueue); + TestEdcaf teardownEdcaf; + teardownEdcaf.pendingQueue = &teardownQueue; + teardownEdcaf.inProgressFrames = &teardownInProgressFrames; + teardownEdcaf.qosAckHandler = &teardownAckHandler; + TestEdca teardownEdca; + teardownEdca.edcaf = &teardownEdcaf; + auto teardownOriginatorHandler = new TestOriginatorHandler(); + auto teardownRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy teardownOriginatorPolicy; + teardownOriginatorPolicy.addbaAccepted = false; + TestRecipientPolicy teardownRecipientPolicy; + TestHcf teardownHcf; + teardownHcf.configureBlockAckHandlers(teardownOriginatorHandler, &teardownOriginatorPolicy, teardownRecipientHandler, &teardownRecipientPolicy); + teardownHcf.configureEligibilityIndex(&teardownEdca, &teardownDataService); + teardownHcf.observePendingQueue(&teardownQueue); + TestCallback teardownCallback; + Packet teardownTrigger("teardownTrigger"); + auto teardownTriggerHeader = makeQosHeader(peer1, removalReason == queueing::IPacketQueue::PacketRemovalReason::DROPPED ? 8 : 9, SequenceNumberCyclic(0)); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownTrigger, teardownTriggerHeader, &teardownOriginatorPolicy, &teardownCallback); + auto teardownRequest = dynamicPtrCast(teardownCallback.managementHeader); + teardownOriginatorHandler->processTransmittedAddbaReq(teardownCallback.managementPacket, teardownRequest, &teardownOriginatorPolicy, &teardownCallback); + auto teardownResponse = makeResponse(peer1, teardownTriggerHeader->getTid(), teardownRequest->getDialogToken(), 0); + TestSignalListener addedListener; + TestSignalListener deletedListener; + teardownHcf.subscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + teardownHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletedListener); + teardownHcf.processReceivedManagementFrame(teardownResponse); + ASSERT(addedListener.numSignals == 1); + ASSERT(deletedListener.numSignals == 1); + ASSERT(teardownOriginatorHandler->getAgreement(peer1, teardownTriggerHeader->getTid()) == nullptr); + ASSERT(teardownHcf.getNumTrackedPendingFrames() == 0); + TestCallback teardownRetryCallback; + Packet teardownRetryTrigger("teardownRetryTrigger"); + auto teardownRetryHeader = makeQosHeader(peer1, teardownTriggerHeader->getTid(), SequenceNumberCyclic(1)); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownRetryTrigger, teardownRetryHeader, &teardownOriginatorPolicy, &teardownRetryCallback); + ASSERT(teardownRetryCallback.managementPacket == nullptr); + teardownOriginatorHandler->allowRetryNow(peer1, teardownTriggerHeader->getTid()); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownRetryTrigger, teardownRetryHeader, &teardownOriginatorPolicy, &teardownRetryCallback); + ASSERT(teardownRetryCallback.managementPacket != nullptr); + teardownHcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + teardownHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletedListener); +} + +OriginatorQosMacDataService dataService; +dataService.setFrameEligibilityFunction([&handler](const Packet *packet) { + auto header = dynamicPtrCast(packet->peekAtFront()); + return header == nullptr || header->getType() != ST_DATA_WITH_QOS || !handler.isAddbaResponsePending(header->getReceiverAddress(), header->getTid()); +}); +Packet heldFrame("heldFrame", makeQosHeader(peer3, 5, SequenceNumberCyclic(0))); +Packet otherTidFrame("otherTidFrame", makeQosHeader(peer3, 4, SequenceNumberCyclic(0))); +Packet otherPeerFrame("otherPeerFrame", makeQosHeader(peer2, 5, SequenceNumberCyclic(0))); +auto managementHeader = makeShared(); +managementHeader->setType(ST_ACTION); +Packet managementFrame("managementFrame", managementHeader); +TestPacketQueue mixedQueue; +mixedQueue.packets = { &heldFrame, &otherTidFrame, &otherPeerFrame, &managementFrame }; +ASSERT(!dataService.isFrameEligible(&heldFrame)); +ASSERT(dataService.isFrameEligible(&otherTidFrame)); +ASSERT(dataService.isFrameEligible(&otherPeerFrame)); +ASSERT(dataService.isFrameEligible(&managementFrame)); +ASSERT(dataService.hasEligibleFrame(&mixedQueue)); +mixedQueue.packets = { &heldFrame }; +ASSERT(!dataService.hasEligibleFrame(&mixedQueue)); + +// HCF availability is a count lookup: rebuilding evaluates each pending frame +// once, while repeated availability checks neither enumerate queues nor +// reevaluate the Block Ack eligibility predicate. +{ + TestOriginatorQosMacDataService indexedDataService; + bool addbaResponsePending = true; + int numEligibilityChecks = 0; + Packet *setupFrame = nullptr; + indexedDataService.setFrameEligibilityFunction([&](const Packet *packet) { + numEligibilityChecks++; + return !addbaResponsePending || packet == setupFrame; + }); + TestAckHandler indexedAckHandler; + TestPacketQueue pendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames inProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf edcafs[AC_NUMCATEGORIES]; + TestEdca indexedEdca; + indexedEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + inProgressFrames[ac].configure(&indexedDataService, &indexedAckHandler); + edcafs[ac].pendingQueue = &pendingQueues[ac]; + edcafs[ac].inProgressFrames = &inProgressFrames[ac]; + indexedEdca.edcafs[ac] = &edcafs[ac]; + } + std::vector indexedPackets; + for (int i = 0; i < 128; i++) { + auto packet = new Packet("heldIndexedFrame", makeShared(B(1))); + indexedPackets.push_back(packet); + pendingQueues[AC_BK].enqueuePacket(packet); + } + auto voiceFrame = new Packet("eligibleVoiceFrame", makeShared(B(1))); + indexedPackets.push_back(voiceFrame); + pendingQueues[AC_VO].enqueuePacket(voiceFrame); + TestHcf indexedHcf; + indexedHcf.configureEligibilityIndex(&indexedEdca, &indexedDataService); + indexedHcf.rebuildEligibilityIndex(); + auto requireIndex = [](bool condition, const char *message) { + if (!condition) + throw cRuntimeError("Eligibility index test failed: %s", message); + }; + requireIndex(indexedHcf.getNumTrackedPendingFrames() == 129, "initial cardinality"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 0, "initial background count"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_VO) == 0, "initial voice count"); + int eligibilityChecksAfterRebuild = numEligibilityChecks; + int queueReadsAfterRebuild = 0; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + queueReadsAfterRebuild += pendingQueues[ac].numGetPacketCalls; + bool allHeldQueriesUnavailable = true; + for (int i = 0; i < 1000; i++) { + bool hasBackgroundFrame = indexedHcf.hasFrame(AC_BK); + bool hasVoiceFrame = indexedHcf.hasFrame(AC_VO); + allHeldQueriesUnavailable &= !hasBackgroundFrame && !hasVoiceFrame; + } + requireIndex(allHeldQueriesUnavailable, "all-held availability"); + ASSERT(allHeldQueriesUnavailable); + bool predicateWasNotReevaluated = numEligibilityChecks == eligibilityChecksAfterRebuild; + requireIndex(predicateWasNotReevaluated, "availability reevaluated the predicate"); + ASSERT(predicateWasNotReevaluated); + int queueReadsAfterQueries = 0; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + queueReadsAfterQueries += pendingQueues[ac].numGetPacketCalls; + bool queueWasNotEnumerated = queueReadsAfterQueries == queueReadsAfterRebuild; + requireIndex(queueWasNotEnumerated, "availability enumerated a pending queue"); + ASSERT(queueWasNotEnumerated); + + auto originalSetupFrame = new Packet("eligibleSetupFrame", makeShared(B(1))); + setupFrame = originalSetupFrame; + indexedPackets.push_back(originalSetupFrame); + indexedHcf.trackFrame(originalSetupFrame, AC_BK); + pendingQueues[AC_BK].enqueuePacket(originalSetupFrame); + bool hasSetupFrame = indexedHcf.hasFrame(AC_BK); + requireIndex(hasSetupFrame, "tracked setup frame unavailable"); + auto replacementFrame = new Packet("eligibleReplacementFrame", makeShared(B(1))); + setupFrame = replacementFrame; + indexedPackets.push_back(replacementFrame); + indexedHcf.trackFrame(replacementFrame, AC_BK); + pendingQueues[AC_BK].enqueuePacket(replacementFrame); + indexedHcf.untrackFrame(indexedPackets.front()); + pendingQueues[AC_BK].removePacket(indexedPackets.front()); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 2, "existing-victim count"); + auto tailDroppedArrival = new Packet("tailDroppedArrival", makeShared(B(1))); + indexedHcf.trackFrame(tailDroppedArrival, AC_BK); + indexedHcf.untrackFrame(tailDroppedArrival); + indexedHcf.untrackFrame(tailDroppedArrival); + delete tailDroppedArrival; + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 2, "tail-drop count"); + indexedHcf.untrackFrame(replacementFrame); + pendingQueues[AC_BK].removePacket(replacementFrame); + indexedHcf.untrackFrame(originalSetupFrame); + pendingQueues[AC_BK].removePacket(originalSetupFrame); + bool hasFrameAfterSelectedRemovals = indexedHcf.hasFrame(AC_BK); + requireIndex(!hasFrameAfterSelectedRemovals, "selected removals left availability"); + + addbaResponsePending = false; + indexedHcf.rebuildEligibilityIndex(); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == pendingQueues[AC_BK].getNumPackets(), "terminal-transition background count"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_VO) == 1, "terminal-transition voice count"); + addbaResponsePending = true; + setupFrame = nullptr; + indexedHcf.rebuildEligibilityIndex(); + bool hasBackgroundFrameAfterCreation = indexedHcf.hasFrame(AC_BK); + bool hasVoiceFrameAfterCreation = indexedHcf.hasFrame(AC_VO); + requireIndex(!hasBackgroundFrameAfterCreation && !hasVoiceFrameAfterCreation, "creation-transition availability"); + auto inProgressFrame = new Packet("inProgressOnlyFrame", makeQosHeader(peer1, 3, SequenceNumberCyclic(1))); + setupFrame = inProgressFrame; + inProgressFrames[AC_VI].addFrame(inProgressFrame); + bool hasInProgressFrame = indexedHcf.hasFrame(AC_VI); + requireIndex(hasInProgressFrame, "eligible in-progress frame unavailable"); + requireIndex(indexedHcf.getNumTrackedPendingFrames() == 128, "in-progress frame entered pending index"); + auto releasedFrames = inProgressFrames[AC_VI].releaseFrames(); + requireIndex(releasedFrames.size() == 1, "in-progress release count"); + delete releasedFrames.front(); + setupFrame = nullptr; + bool hasFrameAfterInProgressRelease = indexedHcf.hasFrame(AC_VI); + requireIndex(!hasFrameAfterInProgressRelease, "released in-progress frame remained available"); + + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].removeAllPackets(); + for (auto packet : indexedPackets) + delete packet; +} + +// IEEE Std 802.11-2024, 10.23.2.4: after the current owner releases the +// channel, Hcf re-arms every AC that has an eligible frame, and no idle AC. +{ + TestOriginatorQosMacDataService armingDataService; + armingDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + TestAckHandler armingAckHandler; + TestPacketQueue armingPendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames armingInProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf armingEdcafs[AC_NUMCATEGORIES]; + TestEdca armingEdca; + armingEdca.numEdcafs = AC_NUMCATEGORIES; + armingEdca.edcaf = nullptr; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + armingInProgressFrames[ac].configure(&armingDataService, &armingAckHandler, &armingPendingQueues[ac]); + armingEdcafs[ac].pendingQueue = &armingPendingQueues[ac]; + armingEdcafs[ac].inProgressFrames = &armingInProgressFrames[ac]; + armingEdca.edcafs[ac] = &armingEdcafs[ac]; + } + auto backgroundFrame = new Packet("armingBackgroundFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))); + auto voiceFrame = new Packet("armingVoiceFrame", makeQosHeader(peer1, 2, SequenceNumberCyclic(1))); + armingInProgressFrames[AC_BK].addOwnedFrame(backgroundFrame); + armingInProgressFrames[AC_VO].addOwnedFrame(voiceFrame); + TestHcf armingHcf; + armingHcf.configureEligibilityIndex(&armingEdca, &armingDataService); + armingHcf.requestEligible(); + ASSERT(armingEdca.requestedAccessCategories.size() == 2); + ASSERT(armingEdca.requestedAccessCategories[0] == AC_BK); + ASSERT(armingEdca.requestedAccessCategories[1] == AC_VO); + auto releasedBackground = armingInProgressFrames[AC_BK].releaseFrames(); + auto releasedVoice = armingInProgressFrames[AC_VO].releaseFrames(); + for (auto frame : releasedBackground) + delete frame; + for (auto frame : releasedVoice) + delete frame; +} + +// The frame-sequence boundary releases the owner and ends its TXOP before +// re-arming eligible ACs; an already-active AC receives the callback without +// restarting contention. +{ + TestOriginatorQosMacDataService sequenceDataService; + sequenceDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + TestAckHandler sequenceAckHandler; + TestPacketQueue sequencePendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames sequenceInProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf sequenceEdcafs[AC_NUMCATEGORIES]; + TestEdca sequenceEdca; + sequenceEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + sequenceInProgressFrames[ac].configure(&sequenceDataService, &sequenceAckHandler, &sequencePendingQueues[ac]); + sequenceEdcafs[ac].pendingQueue = &sequencePendingQueues[ac]; + sequenceEdcafs[ac].inProgressFrames = &sequenceInProgressFrames[ac]; + sequenceEdcafs[ac].setAccessCategory(AccessCategory(ac)); + sequenceEdca.edcafs[ac] = &sequenceEdcafs[ac]; + } + sequenceEdca.edcaf = &sequenceEdcafs[AC_BE]; + sequenceInProgressFrames[AC_BK].addFrame(new Packet("sequenceBackgroundFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(70)))); + sequenceInProgressFrames[AC_VO].addFrame(new Packet("sequenceVoiceFrame", makeQosHeader(peer1, 2, SequenceNumberCyclic(71)))); + sequenceEdcafs[AC_VO].contentionActive = true; + std::vector eventOrder; + sequenceEdcafs[AC_BE].eventOrder = &eventOrder; + TestTxopProcedure sequenceTxop; + sequenceTxop.eventOrder = &eventOrder; + sequenceEdcafs[AC_BE].setTxopProcedure(&sequenceTxop); + sequenceEdca.eventOrder = &eventOrder; + TestHcf sequenceHcf; + sequenceHcf.configureEligibilityIndex(&sequenceEdca, &sequenceDataService); + auto sequenceHandler = new TestFrameSequenceHandler(); + sequenceHcf.configureFrameSequenceHandler(sequenceHandler); + TestMac sequenceMac; + sequenceHcf.configureMac(&sequenceMac); + sequenceHcf.finishSequence(); + ASSERT(eventOrder == std::vector({ 1, 2, 3, 3 })); + ASSERT(sequenceEdca.requestedAccessCategories.size() == 2); + ASSERT(sequenceEdca.requestedAccessCategories[0] == AC_BK); + ASSERT(sequenceEdca.requestedAccessCategories[1] == AC_VO); + ASSERT(sequenceEdcafs[AC_VO].numRequestChannelCalls == 1); + ASSERT(sequenceEdcafs[AC_VO].numContentionStarts == 0); + auto releasedBackground = sequenceInProgressFrames[AC_BK].releaseFrames(); + auto releasedVoice = sequenceInProgressFrames[AC_VO].releaseFrames(); + for (auto frame : releasedBackground) + delete frame; + for (auto frame : releasedVoice) + delete frame; +} + +// IEEE Std 802.11-2024, 10.23.2.4: a collided AC with no eligible frame is +// not a handled collision and must not mutate recovery state or be counted. +{ + TestEdcaf emptyCollidedEdcaf; + emptyCollidedEdcaf.setAccessCategory(AC_BK); + TestInProgressFrames emptyInProgressFrames; + TestOriginatorQosMacDataService emptyDataService; + TestAckHandler emptyAckHandler; + TestPacketQueue emptyPendingQueue; + emptyInProgressFrames.configure(&emptyDataService, &emptyAckHandler, &emptyPendingQueue); + emptyCollidedEdcaf.inProgressFrames = &emptyInProgressFrames; + TestHcf collisionHcf; + ASSERT(collisionHcf.processInternalCollision({ &emptyCollidedEdcaf }) == 0); +} + +// A channel grant whose collision-controller vector contains only an AC with +// no eligible frame releases the channel without opening a TXOP or emitting a +// phantom collision statistic. +{ + TestPacketQueue grantedPendingQueue; + TestOriginatorQosMacDataService grantedDataService; + TestAckHandler grantedAckHandler; + TestInProgressFrames grantedInProgressFrames; + grantedInProgressFrames.configure(&grantedDataService, &grantedAckHandler, &grantedPendingQueue); + TestEdcaf grantedEdcaf; + grantedEdcaf.setAccessCategory(AC_BE); + grantedEdcaf.pendingQueue = &grantedPendingQueue; + grantedEdcaf.inProgressFrames = &grantedInProgressFrames; + TestEdca grantedEdca; + grantedEdca.edcaf = &grantedEdcaf; + grantedEdca.internallyCollidedEdcafs = { &grantedEdcaf }; + TestHcf grantedHcf; + grantedHcf.configure(&grantedEdca); + TestMac grantedMac; + grantedHcf.configureMac(&grantedMac); + TestSignalListener collisionListener; + grantedHcf.subscribe(Hcf::edcaCollisionDetectedSignal, &collisionListener); + grantedHcf.grantChannel(&grantedEdcaf); + ASSERT(grantedEdcaf.numReleaseChannelCalls == 1); + ASSERT(grantedMac.numPendingRadioConfigSends == 1); + ASSERT(collisionListener.numSignals == 0); + grantedHcf.unsubscribe(Hcf::edcaCollisionDetectedSignal, &collisionListener); +} + // A direct IPacketQueue implementation without OMNeT++ signals cannot leave // the index stale because ownership departures use the typed queue callback. { @@ -2065,6 +3197,192 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); secondQueue.removePacket(otherInputPacket); delete otherInputPacket; } + +// Discarding a stale setup frame clears its per-frame retry state without +// emitting the retry-limit semantic event. +{ + TestNonQosRecoveryProcedure recoveryProcedure; + TestSignalListener retryLimitListener; + recoveryProcedure.subscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + auto header = makeShared(); + header->setSequenceNumber(SequenceNumberCyclic(42)); + Packet packet("staleRecoveryPacket", header); + recoveryProcedure.addRetry(header); + ASSERT(recoveryProcedure.hasRetry(header)); + recoveryProcedure.discardFrame(&packet, header); + ASSERT(!recoveryProcedure.hasRetry(header)); + ASSERT(retryLimitListener.numSignals == 0); + recoveryProcedure.unsubscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); +} + + +// Either terminal discard path clears both per-frame retry maps. Reusing the +// same sequence/fragment identity therefore starts from zero after either +// data-first or RTS-first failure history. +{ + TestNonQosRecoveryProcedure recoveryProcedure; + auto header = makeShared(); + header->setSequenceNumber(SequenceNumberCyclic(43)); + Packet shortPacket("shortTerminalDiscard", header); + recoveryProcedure.addShortRetry(header, 2); + recoveryProcedure.addLongRetry(header, 3); + ASSERT(recoveryProcedure.getShortRetryCount(&shortPacket, header) == 2); + ASSERT(recoveryProcedure.getLongRetryCount(&shortPacket, header) == 3); + recoveryProcedure.discardFrame(&shortPacket, header); + ASSERT(recoveryProcedure.getShortRetryCount(&shortPacket, header) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(&shortPacket, header) == 0); + + Packet reusedPacket("reusedTerminalDiscard", header); + recoveryProcedure.addLongRetry(header, 4); + recoveryProcedure.addShortRetry(header, 5); + ASSERT(recoveryProcedure.getShortRetryCount(&reusedPacket, header) == 5); + ASSERT(recoveryProcedure.getLongRetryCount(&reusedPacket, header) == 4); + recoveryProcedure.discardRtsFrame(header); + ASSERT(recoveryProcedure.getShortRetryCount(&reusedPacket, header) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(&reusedPacket, header) == 0); +} + +// A stale RTS-protected ADDBA request uses the management header for its retry +// check and clears the RTS short-retry entry even when the protected frame is +// long. It is a transaction discard, not a retry-limit or link-break event. +{ + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(10, 4, &cwCalculator); + QosAckHandler ackHandler; + TestOriginatorQosMacDataService originatorDataService; + TestInProgressFrames activeFrames; + activeFrames.configure(&originatorDataService, &ackHandler); + TestEdcaf edcaf; + edcaf.inProgressFrames = &activeFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + auto staleHandler = new TestOriginatorHandler(); + TestHcf hcf; + hcf.configureTransmittedData(&edca, staleHandler, &originatorPolicy); + TestSignalListener packetDropListener; + TestSignalListener linkBrokenListener; + hcf.subscribe(packetDroppedSignal, &packetDropListener); + hcf.subscribe(linkBrokenSignal, &linkBrokenListener); + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setTid(3); + request->setDialogToken(91); + request->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet("staleRtsProtectedAddba", request); + packet->insertAtBack(makeShared(B(100))); + ASSERT(packet->getByteLength() >= 10); + activeFrames.addFrame(packet); + ackHandler.processTransmittedDataOrMgmtFrame(request); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + hcf.processRtsProtectionFailure(packet); + ASSERT(recoveryProcedure.getShortRetryCount(packet, request) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(packet, request) == 0); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(packetDropListener.numSignals == 1); + ASSERT(linkBrokenListener.numSignals == 0); + hcf.unsubscribe(packetDroppedSignal, &packetDropListener); + hcf.unsubscribe(linkBrokenSignal, &linkBrokenListener); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + delete releasedFrames.front(); +} + +// At the RTS retry limit, the same long protected management frame still +// clears its short-retry entry and emits each terminal event exactly once. +{ + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(10, 1, &cwCalculator); + QosAckHandler ackHandler; + TestOriginatorQosMacDataService originatorDataService; + TestInProgressFrames activeFrames; + activeFrames.configure(&originatorDataService, &ackHandler); + TestEdcaf edcaf; + edcaf.inProgressFrames = &activeFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestHcf hcf; + TestMac retryLimitedRtsProtectedAddbaMac; + hcf.configureMac(&retryLimitedRtsProtectedAddbaMac); + hcf.configureTransmittedData(&edca, new TestOriginatorHandler(), &originatorPolicy); + TestSignalListener retryLimitListener; + TestSignalListener packetDropListener; + TestSignalListener linkBrokenListener; + recoveryProcedure.subscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + hcf.subscribe(packetDroppedSignal, &packetDropListener); + hcf.subscribe(linkBrokenSignal, &linkBrokenListener); + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setTid(3); + request->setDialogToken(92); + request->setSequenceNumber(SequenceNumberCyclic(78)); + auto packet = new Packet("retryLimitedRtsProtectedAddba", request); + packet->insertAtBack(makeShared(B(100))); + ASSERT(packet->getByteLength() >= 10); + activeFrames.addFrame(packet); + ackHandler.processTransmittedDataOrMgmtFrame(request); + hcf.processRtsProtectionFailure(packet); + ASSERT(recoveryProcedure.getShortRetryCount(packet, request) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(packet, request) == 0); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(retryLimitListener.numSignals == 1); + ASSERT(packetDropListener.numSignals == 1); + ASSERT(linkBrokenListener.numSignals == 1); + recoveryProcedure.unsubscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + hcf.unsubscribe(packetDroppedSignal, &packetDropListener); + hcf.unsubscribe(linkBrokenSignal, &linkBrokenListener); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + delete releasedFrames.front(); +} + +// A frame prefetched for duration calculation already has an SSN, but it must +// remain in progress without being selected while the same transaction is pending. +TestAckHandler ackHandler; +TestInProgressFrames inProgressFrames; +inProgressFrames.configure(&dataService, &ackHandler); +auto prefetchedHeldFrame = new Packet("prefetchedHeldFrame", makeQosHeader(peer3, 5, SequenceNumberCyclic(21))); +auto prefetchedOtherTidFrame = new Packet("prefetchedOtherTidFrame", makeQosHeader(peer3, 4, SequenceNumberCyclic(1))); +inProgressFrames.addFrame(prefetchedHeldFrame); +inProgressFrames.addFrame(prefetchedOtherTidFrame); +ASSERT(inProgressFrames.hasEligibleFrame()); +ASSERT(inProgressFrames.getFrameToTransmit() == prefetchedOtherTidFrame); + +// Repeated cleanup requests move an in-progress frame to the discard list once. +{ + TestInProgressFrames idempotentDropFrames; + auto frame = new Packet("idempotentDropFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(22))); + idempotentDropFrames.addFrame(frame); + idempotentDropFrames.dropFrame(frame); + idempotentDropFrames.dropFrame(frame); + auto releasedFrames = idempotentDropFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + ASSERT(releasedFrames.front() == frame); + delete frame; +} + +// DCF sequence predicates materialize the next eligible pending frame. +TestPacketQueue deferredQueue; +auto deferredFrame = new Packet("deferredFrame", makeQosHeader(peer2, 4, SequenceNumberCyclic(123))); +deferredQueue.packets = { deferredFrame }; +TestOriginatorQosMacDataService deferredDataService; +TestInProgressFrames deferredInProgressFrames; +deferredInProgressFrames.configure(&deferredDataService, &ackHandler, &deferredQueue); +bool hasDeferredInProgressFrame = deferredInProgressFrames.hasInProgressFrames(); +if (!hasDeferredInProgressFrame || deferredQueue.getNumPackets() != 0) + throw cRuntimeError("In-progress frame predicate did not materialize the eligible pending frame"); +ASSERT(hasDeferredInProgressFrame); +ASSERT(deferredQueue.getNumPackets() == 0); +ASSERT(deferredFrame->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(123)); +auto deferredFrames = deferredInProgressFrames.releaseFrames(); +ASSERT(deferredFrames.size() == 1); +delete deferredFrames.at(0); + // A real queue with a downstream collector predicate-dequeues a non-front // packet, records its residence and semantic pull once, and animates it to the // collector while retaining the front packet. @@ -2550,22 +3868,98 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); secondQueue->deleteModule(); bufferModule->deleteModule(); } -// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. -RecipientBlockAckAgreementHandler immediateRecipientHandler; -TestRecipientPolicy recipientPolicy; -TestCallback recipientCallback; -auto recipientRequest = makeShared(); -recipientRequest->setTransmitterAddress(peer1); -recipientRequest->setTid(6); -recipientRequest->setDialogToken(77); -recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(0)); -recipientRequest->setBufferSize(64); -recipientRequest->setBlockAckPolicy(1); -recipientRequest->setBlockAckTimeoutValue(0); -immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback); -auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); -ASSERT(acceptedResponse->getDialogToken() == 77); -ASSERT(acceptedResponse->getStatusCode() == 0); + +// The multicast continuation predicate materializes its candidate once and +// reuses that result for both the TXOP and receiver-address decisions. +{ + TestOriginatorQosMacDataService multicastDataService; + TestPacketQueue multicastPendingQueue; + TestAckHandler multicastAckHandler; + auto multicastFrame = new Packet("multicastTxopFrame", makeQosHeader(MacAddress::BROADCAST_ADDRESS, 1, SequenceNumberCyclic(4))); + multicastPendingQueue.packets = { multicastFrame }; + TestInProgressFrames multicastInProgressFrames; + multicastInProgressFrames.configure(&multicastDataService, &multicastAckHandler, &multicastPendingQueue); + TestTxopProcedure multicastTxopProcedure; + FrameSequenceContext multicastContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &multicastInProgressFrames, nullptr, nullptr, nullptr, new QoSContext(nullptr, nullptr, nullptr, &multicastTxopProcedure)); + HcfFs multicastHcfFs; + RepeatingFs multicastRepeatingFs(new TestEmptyFrameSequence(), [](RepeatingFs *, FrameSequenceContext *) { return false; }); + ASSERT(multicastHcfFs.hasMoreTxOpsAndMulticast(&multicastRepeatingFs, &multicastContext)); + ASSERT(multicastPendingQueue.numSelectedDequeues == 1); + ASSERT(multicastPendingQueue.getNumPackets() == 0); + auto multicastFrames = multicastInProgressFrames.releaseFrames(); + ASSERT(multicastFrames == std::vector({ multicastFrame })); + delete multicastFrame; +} + +// At the TXOP continuation boundary, an eligible pending frame is +// materialized by the real repeating-sequence lifecycle. +{ + TestOriginatorQosMacDataService txopDataService; + TestPacketQueue txopPendingQueue; + auto initialTxopFrame = new Packet("initialTxopFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(2))); + auto continuedTxopFrame = new Packet("continuedTxopFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(3))); + txopPendingQueue.packets = { continuedTxopFrame }; + TestInProgressFrames txopInProgressFrames; + txopInProgressFrames.configure(&txopDataService, &ackHandler, &txopPendingQueue); + txopInProgressFrames.addFrame(initialTxopFrame); + TestTxopProcedure txopProcedure; + FrameSequenceContext txopContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &txopInProgressFrames, nullptr, nullptr, nullptr, new QoSContext(nullptr, nullptr, nullptr, &txopProcedure)); + HcfFs hcfFs; + RepeatingFs repeatingFs(new TestEmptyFrameSequence(), [&hcfFs](RepeatingFs *frameSequence, FrameSequenceContext *context) { return hcfFs.hasMoreTxOps(frameSequence, context); }); + repeatingFs.startSequence(&txopContext, 0); + ASSERT(repeatingFs.getCount() == 1); + ASSERT(txopPendingQueue.getNumPackets() == 1); + txopInProgressFrames.dropFrame(initialTxopFrame); + repeatingFs.repeatSequence(&txopContext); + ASSERT(repeatingFs.getCount() == 2); + ASSERT(txopPendingQueue.getNumPackets() == 0); + ASSERT(txopPendingQueue.numSelectedDequeues == 1); + ASSERT(txopInProgressFrames.getFrameToTransmit() == continuedTxopFrame); + auto txopFrames = txopInProgressFrames.releaseFrames(); + ASSERT(txopFrames.size() == 2); + for (auto frame : txopFrames) + delete frame; +} + +// Legacy DCF materializes its first queued frame before starting DcfFs, and a +// stale empty grant is released without dereferencing a null frame. +{ + TestOriginatorQosMacDataService dcfDataService; + TestPacketQueue dcfPendingQueue; + TestInProgressFrames dcfInProgressFrames; + dcfInProgressFrames.configure(&dcfDataService, &ackHandler, &dcfPendingQueue); + TestDcaf dcaf; + dcaf.configure(&dcfInProgressFrames); + TestDcf dcf; + TestMac dcfMac; + auto dcfHandler = dcf.configure(&dcaf, &dcfMac); + dcf.grant(&dcaf); + ASSERT(dcaf.numReleasedChannels == 1); + ASSERT(dcfMac.numPendingRadioConfigSends == 1); + ASSERT(dcfHandler->numStartedSequences == 0); +} +{ + TestOriginatorQosMacDataService dcfDataService; + TestPacketQueue dcfPendingQueue; + auto firstDcfFrame = new Packet("firstDcfFrame", makeQosHeader(peer1, 0, SequenceNumberCyclic(0))); + dcfPendingQueue.packets = { firstDcfFrame }; + TestInProgressFrames dcfInProgressFrames; + dcfInProgressFrames.configure(&dcfDataService, &ackHandler, &dcfPendingQueue); + TestDcaf dcaf; + dcaf.configure(&dcfInProgressFrames); + TestDcf dcf; + TestMac dcfMac; + auto dcfHandler = dcf.configure(&dcaf, &dcfMac); + dcf.grant(&dcaf); + ASSERT(dcaf.numReleasedChannels == 0); + ASSERT(dcfMac.numPendingRadioConfigSends == 0); + ASSERT(dcfHandler->numStartedSequences == 1); + ASSERT(dcfPendingQueue.getNumPackets() == 0); + ASSERT(dcfInProgressFrames.getFrameToTransmit() == firstDcfFrame); + auto dcfFrames = dcfInProgressFrames.releaseFrames(); + ASSERT(dcfFrames.size() == 1); + delete dcfFrames.at(0); +} EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; From e3f5f616b55f208aab08f91e62a705cb4e566260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:41:09 +0200 Subject: [PATCH 31/91] ieee80211: add+fix: reassemble and expire fragmented MAC frames A fragmented action body cannot be interpreted as a complete ADDBA or DELBA. Preserve action context while fragments are queued and retried, and dispatch management actions only after successful reassembly. Shared fragment reassembly now expires incomplete bodies after the configured receive lifetime (512 TUs by default); late bodies are discarded before dispatch. Six rows change (16 values) because incomplete fragment bodies now expire after 512 TUs instead of completing late. Extending only the diagnostic lifetime beyond the run duration restores all 24 preceding values in those rows; expiry tests establish the intended drop behavior. The user accepted these exact source-owned expectations on 2026-09-15. Validation: diagnostic debug and release builds; 9 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.add+change.fix | fingerprint test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 8 + doc/src/migration-guide/index.rst | 16 + .../mac/Ieee80211MacHeaderSerializer.cc | 15 + .../mac/Ieee80211MacProtocolDissector.cc | 4 +- .../OriginatorBlockAckAgreementHandler.cc | 11 +- .../RecipientBlockAckAgreementHandler.cc | 4 + .../ieee80211/mac/contract/IReassembly.h | 17 + .../contract/IRecipientQosMacDataService.h | 6 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 24 +- .../fragmentation/BasicFragmentationPolicy.cc | 27 +- .../mac/fragmentation/BasicReassembly.cc | 76 +++- .../mac/fragmentation/BasicReassembly.h | 17 +- .../mac/fragmentation/Defragmentation.cc | 13 + .../mac/fragmentation/Fragmentation.cc | 55 ++- .../Ieee80211FragmentedActionContextTag.cc | 15 + .../Ieee80211FragmentedActionContextTag.h | 48 +++ .../mac/recipient/RecipientMacDataService.cc | 33 +- .../mac/recipient/RecipientMacDataService.h | 4 + .../mac/recipient/RecipientMacDataService.ned | 1 + .../recipient/RecipientQosMacDataService.cc | 61 ++- .../recipient/RecipientQosMacDataService.h | 4 + .../recipient/RecipientQosMacDataService.ned | 1 + tests/fingerprint/examples.csv | 8 +- tests/fingerprint/showcases.csv | 4 +- tests/unit/Ieee80211AddbaTransaction_1.test | 408 ++++++++++++++++-- 25 files changed, 792 insertions(+), 88 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc create mode 100644 src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h diff --git a/WHATSNEW b/WHATSNEW index 5d1e61e9cb1..d74f2291414 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -373,6 +373,14 @@ Notable backward incompatible changes are the following: trajectories. A-MSDU service now appears as dequeued traffic in queue counters and graphical fingerprints, instead of administrative removal. +7. Fragment Reassembly and Receive Lifetime + + Fragmented management actions are dispatched only after their complete body is + reassembled. Shared fragment reassembly now expires incomplete bodies after + maxReceiveLifetime, whose default is 512 TUs. Late fragments can therefore be dropped + instead of completing an old body; affected application deliveries, forwarding + trajectories and fingerprints change. + 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 1d89e27fbcd..8b097fe5c5c 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -114,6 +114,22 @@ response must not establish the agreement again or reset its receive window. Use returned ownership-bearing teardown result when publishing a deleted-agreement notification. +Fragment Reassembly and Receive Lifetime +---------------------------------------- + +``RecipientMacDataService`` and ``RecipientQosMacDataService`` now expose +``maxReceiveLifetime``, defaulting to ``524288us`` (512 TUs). Incomplete bodies are +discarded when that receive lifetime expires. Models that relied on arbitrarily late +completion will deliver fewer packets; select a deliberate lifetime for the study rather +than treating the previous unbounded retention as a guarantee. + +Custom recipient services returning ``ManagementFrameReceptionResult`` must supply +``completeHeader`` only when a complete management body is available. A duplicate +fragment can be acknowledged without dispatching ADDBA or DELBA. Preserve local action +context across fragmented transmission, reconstruct the complete action body, and +dispatch its subtype only after reassembly. Fragmented on-air management headers do not +carry a complete action body in every fragment. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index 2a482cded2f..50b149538f1 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -238,6 +238,10 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c break; } auto actionFrame = dynamicPtrCast(chunk); + // A fragmented action MPDU uses a generic management header; + // its action-body slice is a separate packet chunk. + if (actionFrame == nullptr) + break; switch (actionFrame->getCategory()) { case 3: { stream.writeByte(actionFrame->getCategory()); @@ -503,6 +507,17 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre actionFrame->setSequenceNumber(sequenceNumber); if (order) stream.readUint32Be(); + if (actionFrame->getMoreFragments() || actionFrame->getFragmentNumber() != 0) { + auto mgmtHeader = makeShared(); + copyBasicFields(mgmtHeader, macHeader); + mgmtHeader->setDurationField(actionFrame->getDurationField()); + mgmtHeader->setReceiverAddress(actionFrame->getReceiverAddress()); + mgmtHeader->setTransmitterAddress(actionFrame->getTransmitterAddress()); + mgmtHeader->setAddress3(actionFrame->getAddress3()); + mgmtHeader->setFragmentNumber(actionFrame->getFragmentNumber()); + mgmtHeader->setSequenceNumber(actionFrame->getSequenceNumber()); + return mgmtHeader; + } // Only the Block Ack category is modelled. For any other category or action, // preserve the entire action body (Category + Action + parameters) verbatim in // an Ieee80211ActionFrameOther so the frame round-trips without parsing it. The diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc index f6cb4108c37..2b73dc33db1 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc @@ -95,7 +95,9 @@ void Ieee80211MacProtocolDissector::dissect(Packet *packet, const Protocol *prot // header, so its serializer is exercised instead of leaving the body as raw // bytes; unknown subtypes fall back to the generic mgmt dissector using namespace inet::ieee80211; - if (packet->getDataLength() > b(0)) { + if (mgmtHeader->getMoreFragments() || mgmtHeader->getFragmentNumber() != 0) + callback.dissectPacket(packet, nullptr); + else if (packet->getDataLength() > b(0)) { switch (mgmtHeader->getType()) { case ST_BEACON: callback.visitChunk(packet->popAtFront(), &Protocol::ieee80211Mgmt); break; case ST_PROBEREQUEST: callback.visitChunk(packet->popAtFront(), &Protocol::ieee80211Mgmt); break; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2d91fdaa91c..89606d3fcb7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -11,6 +11,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { @@ -338,7 +339,11 @@ void OriginatorBlockAckAgreementHandler::processDroppedAddbaReq(Packet *packet, std::unique_ptr OriginatorBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); + // IEEE Std 802.11-2024, 10.4 and 11.5.3.2: an untagged DELBA MMPDU + // likewise cannot tear down the agreement before its final fragment. + if (delba->getMoreFragments()) + return nullptr; auto transactionTag = packet->findTag(); if (transactionTag != nullptr) { auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); @@ -364,7 +369,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator() || delba->getMoreFragments()) return false; auto transactionTag = packet->findTag(); @@ -381,7 +386,7 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) return false; auto transactionTag = packet->findTag(); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index adf02821b4d..39528886313 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -172,6 +172,10 @@ void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { + // IEEE Std 802.11-2024, 10.4 and 11.5.3.5: the DELBA MMPDU has not + // been transmitted while a later fragment is still outstanding. + if (delba->getMoreFragments()) + return nullptr; return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 23796ec3556..571dc3fc226 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h @@ -8,9 +8,14 @@ #ifndef __INET_IREASSEMBLY_H #define __INET_IREASSEMBLY_H +#include + #include "inet/linklayer/common/MacAddress.h" namespace inet { + +class Packet; + namespace ieee80211 { class Ieee80211DataOrMgmtHeader; @@ -32,6 +37,18 @@ class INET_API IReassembly */ virtual Packet *addFragment(Packet *frame) = 0; + /** + * Return the earliest receive-lifetime deadline among incomplete frames, + * or SIMTIME_MAX when the reassembly buffer is empty. + */ + virtual simtime_t getNextExpirationTime() const = 0; + + /** + * Remove incomplete frames whose receive lifetime has elapsed and return + * their fragments to the caller for drop signaling and deletion. + */ + virtual std::vector removeExpiredFragments(simtime_t currentTime) = 0; + /** * Discard fragments from the reassembly buffer. Frames are identified by the transmitter * address, the TID, and the sequence number range [startSeqNumber, endSeqNumber[. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 4ddd98ad97e..6a0696a2fb3 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -18,11 +18,11 @@ namespace ieee80211 { class INET_API IRecipientQosMacDataService { public: - // A duplicate has already been consumed and must not be processed as a - // management body; the coordination function may apply a subtype-specific - // response rule using the duplicate flag. struct ManagementFrameReceptionResult { std::vector completeFrames; + // IEEE Std 802.11-2024, 10.5: a management body becomes available to + // the coordination function only after the complete MMPDU is present. + Ptr completeHeader; bool duplicate = false; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 11338db6692..52b2cf7699f 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -17,6 +17,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" @@ -70,9 +71,9 @@ void Hcf::initialize(int stage) originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { - if (auto addbaReq = dynamicPtrCast(packet->peekAtFront())) + if (auto addbaReq = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); - if (auto delba = dynamicPtrCast(packet->peekAtFront())) + if (auto delba = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); auto dataHeader = dynamicPtrCast(packet->peekAtFront()); // Hold this peer/TID while its ADDBA response is pending so no @@ -133,7 +134,7 @@ void Hcf::rebuildPendingFrameEligibility() bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) { if (originatorBlockAckAgreementHandler) { - auto addbaReq = dynamicPtrCast(packet->peekAtFront()); + auto addbaReq = findFragmentedActionContext(packet); if (addbaReq != nullptr && originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq)) { originatorBlockAckAgreementHandler->processDroppedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); rebuildPendingFrameEligibility(); @@ -146,7 +147,7 @@ bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) { if (originatorBlockAckAgreementHandler) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { rebuildPendingFrameEligibility(); return true; @@ -532,7 +533,8 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { auto receptionResult = recipientDataService->managementFrameReceived(packet, mgmtHeader); sendUp(receptionResult.completeFrames); - recipientProcessReceivedManagementFrame(mgmtHeader, receptionResult.duplicate); + if (receptionResult.completeHeader != nullptr) + recipientProcessReceivedManagementFrame(receptionResult.completeHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -655,7 +657,7 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else throw cRuntimeError("Unknown frame"); // TODO QoSDataFrame, NonQoSDataFrame - auto addbaRequest = dynamicPtrCast(protectedHeader); + auto addbaRequest = findFragmentedActionContext(packet); bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaRequest); if (retryLimitReached || staleAddbaRequest) { if (retryLimitReached) { @@ -731,13 +733,13 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< auto edcaf = edca->getEdcaf(ac); if (originatorAckPolicy->isAckNeeded(mgmtHeader)) edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(mgmtHeader); - if (auto addbaReq = dynamicPtrCast(mgmtHeader)) { + if (auto addbaReq = findFragmentedActionContext(packet)) { if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } - else if (dynamicPtrCast(mgmtHeader)) + else if (findFragmentedActionContext(packet)) ; // Recipient agreement was established when the successful response was formed. - else if (auto delba = dynamicPtrCast(mgmtHeader)) { + else if (auto delba = findFragmentedActionContext(packet)) { if (delba->getInitiator()) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); @@ -805,7 +807,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else throw cRuntimeError("Unknown frame"); // TODO qos, nonqos - auto addbaRequest = dynamicPtrCast(failedHeader); + auto addbaRequest = findFragmentedActionContext(failedPacket); bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(failedPacket, addbaRequest); if (retryLimitReached || staleAddbaRequest) { if (retryLimitReached) { @@ -898,7 +900,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); - if (auto delba = dynamicPtrCast(lastTransmittedHeader)) { + if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) rebuildPendingFrameEligibility(); } diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index e47ce922aab..aa741a61a3f 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -29,23 +29,38 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) std::vector sizes; int payloadLength = 0; int headerLength = 0; - // Mgmt frames don't have payload const auto& header = frame->peekAtFront(); + // IEEE Std 802.11-2024, 10.4: only individually addressed MPDUs + // carrying an MSDU or MMPDU are eligible for fragmentation. + if (header->getReceiverAddress().isMulticast()) + return {}; const auto& trailer = frame->peekAtBack(B(4)); int trailerLength = trailer->getChunkLength().get(); if (dynamicPtrCast(header)) { headerLength = header->getChunkLength().get(); payloadLength = frame->getByteLength() - headerLength - trailerLength; } + else if (dynamicPtrCast(header)) { + // Management subclasses currently combine the common MAC header + // and typed MMPDU body. Only the common header is repeated. + headerLength = makeShared()->getChunkLength().get(); + payloadLength = frame->getByteLength() - headerLength - trailerLength; + } else - headerLength = frame->getByteLength(); + return {}; int maxFragmentPayload = fragmentationThreshold - headerLength - trailerLength; + if (maxFragmentPayload <= 0) + throw cRuntimeError("Fragmentation threshold %d is not larger than the %d byte header and trailer", fragmentationThreshold, headerLength + trailerLength); + // IEEE Std 802.11-2024, 10.4: all non-final fragments have the same + // even number of body octets; only the final fragment may be odd. + maxFragmentPayload &= ~1; + if (maxFragmentPayload == 0) + throw cRuntimeError("Fragmentation threshold %d leaves no even-length fragment body", fragmentationThreshold); if (payloadLength > maxFragmentPayload * MAX_NUM_FRAGMENTS) throw cRuntimeError("Fragmentation: frame \"%s\" too large, won't fit into %d fragments", frame->getName(), MAX_NUM_FRAGMENTS); - for (int i = 0; headerLength + trailerLength + payloadLength > fragmentationThreshold; i++) { - auto size = fragmentationThreshold - headerLength - trailerLength; - EV_TRACE << "Computed fragment: i = " << i << ", size = " << size << ".\n"; - sizes.push_back(size); + for (int i = 0; payloadLength > maxFragmentPayload; i++) { + EV_TRACE << "Computed fragment: i = " << i << ", size = " << maxFragmentPayload << ".\n"; + sizes.push_back(maxFragmentPayload); payloadLength -= maxFragmentPayload; } if (payloadLength != 0) { diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 2f32f0207b9..95f4a35bfe3 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -26,12 +26,11 @@ Packet *BasicReassembly::addFragment(Packet *packet) // Frame is not fragmented if (!header->getMoreFragments() && header->getFragmentNumber() == 0) return packet; - // FIXME temporary fix for mgmt frames - if (dynamicPtrCast(header)) - return packet; // find entry for this frame Key key; key.macAddress = header->getTransmitterAddress(); + key.receiverAddress = header->getReceiverAddress(); + key.type = header->getType(); key.tid = -1; if (header->getType() == ST_DATA_WITH_QOS) if (const Ptr& qosDataHeader = dynamicPtrCast(header)) @@ -39,7 +38,25 @@ Packet *BasicReassembly::addFragment(Packet *packet) key.seqNum = header->getSequenceNumber().get(); short fragNum = header->getFragmentNumber(); ASSERT(fragNum >= 0 && fragNum < MAX_NUM_FRAGMENTS); - auto& value = fragmentsMap[key]; + auto it = fragmentsMap.find(key); + if (it != fragmentsMap.end() && it->second.expired) { + // A non-Retry fragment 0 can be a new MMPDU after sequence-number + // reuse. All other fragments of the expired MMPDU are discarded. + if (fragNum == 0 && !header->getRetry()) { + fragmentsMap.erase(it); + it = fragmentsMap.end(); + } + else { + delete packet; + return nullptr; + } + } + if (it == fragmentsMap.end()) { + Value value; + value.receptionStartTime = simTime(); + it = fragmentsMap.emplace(key, value).first; + } + auto& value = it->second; value.fragments.resize(16); // update entry @@ -71,28 +88,47 @@ Packet *BasicReassembly::addFragment(Packet *packet) return nullptr; } +simtime_t BasicReassembly::getNextExpirationTime() const +{ + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& entry : fragmentsMap) + if (!entry.second.expired) + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} + +std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (auto& entry : fragmentsMap) { + auto& value = entry.second; + if (!value.expired && currentTime >= value.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : value.fragments) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + value.fragments.clear(); + value.receivedFragments = 0; + value.allFragments = 0; + value.expired = true; + } + } + return expiredFragments; +} + void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) { - Key key; - key.macAddress = address; - key.tid = tid; - key.seqNum = startSeqNumber; - auto itStart = fragmentsMap.lower_bound(key); - key.seqNum = endSeqNumber; - auto itEnd = fragmentsMap.upper_bound(key); - - if (endSeqNumber < startSeqNumber) { - for (auto it = itStart; it != fragmentsMap.end();) { + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto sequenceNumber = it->first.seqNum; + bool isInSequenceRange = startSeqNumber <= endSeqNumber ? + sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : + sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; + if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange) { for (auto fragment : it->second.fragments) delete fragment; it = fragmentsMap.erase(it); } - itStart = fragmentsMap.begin(); - } - for (auto it = itStart; it != itEnd;) { - for (auto fragment : it->second.fragments) - delete fragment; - it = fragmentsMap.erase(it); + else + it++; } } diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index 0c1d7d810cc..bb0dd20ba71 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,7 +8,10 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include + #include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" #include "inet/linklayer/ieee80211/mac/contract/IReassembly.h" @@ -20,23 +23,33 @@ class INET_API BasicReassembly : public IReassembly, public cObject protected: struct Key { MacAddress macAddress; + MacAddress receiverAddress; + Ieee80211FrameType type; Tid tid; SequenceNumber seqNum; - bool operator==(const Key& o) const { return macAddress == o.macAddress && tid == o.tid && seqNum == o.seqNum; } - bool operator<(const Key& o) const { return macAddress < o.macAddress || (macAddress == o.macAddress && (tid < o.tid || (tid == o.tid && seqNum < o.seqNum))); } + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, seqNum); } + bool operator==(const Key& other) const { return asTuple() == other.asTuple(); } + bool operator<(const Key& other) const { return asTuple() < other.asTuple(); } }; struct Value { std::vector fragments; uint16_t receivedFragments = 0; // each bit corresponds to a fragment number uint16_t allFragments = 0; // bits for all fragments set to one (0..numFragments-1); 0 means unfilled + simtime_t receptionStartTime; + bool expired = false; }; typedef std::map FragmentsMap; FragmentsMap fragmentsMap; + simtime_t maxReceiveLifetime; public: + BasicReassembly() : maxReceiveLifetime(SimTime(512 * 1024, SIMTIME_US)) {} + BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BasicReassembly(); virtual Packet *addFragment(Packet *packet) override; virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; + virtual simtime_t getNextExpirationTime() const override; + virtual std::vector removeExpiredFragments(simtime_t currentTime) override; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc index 091c635504a..2faddfc44a2 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { @@ -33,6 +34,18 @@ Packet *Defragmentation::defragmentFrames(std::vector *fragmentFrames) defragmentedHeader->setMoreFragments(false); defragmentedFrame->insertAtFront(defragmentedHeader); defragmentedFrame->insertAtBack(makeShared()); + if (defragmentedHeader->getType() == ST_ACTION && defragmentedHeader->getChunkLength() == makeShared()->getChunkLength()) { + // Decode the completed action from the reassembled on-air bytes; an + // individual fragment intentionally has no typed action body. + const auto& trailer = defragmentedFrame->popAtBack(B(4)); + auto decodedFrame = new Packet(defragmentedFrame->getName(), defragmentedFrame->peekDataAsBytes()); + decodedFrame->insertAtBack(trailer); + decodedFrame->copyTags(*defragmentedFrame); + decodedFrame->getRegionTags().copyTags(defragmentedFrame->getRegionTags(), defragmentedFrame->getFrontOffset(), decodedFrame->getFrontOffset(), defragmentedFrame->getDataLength()); + decodedFrame->peekAtFront(); + delete defragmentedFrame; + defragmentedFrame = decodedFrame; + } EV_TRACE << "Created " << *defragmentedFrame << ".\n"; return defragmentedFrame; } diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc index 96ed3de0d9b..4465c22ecf3 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc @@ -7,11 +7,40 @@ #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" namespace inet { namespace ieee80211 { +namespace { + +const Ptr copyManagementHeader(const Ptr& source) +{ + auto destination = makeShared(); + destination->setType(source->getType()); + destination->setToDS(source->getToDS()); + destination->setFromDS(source->getFromDS()); + destination->setMoreFragments(source->getMoreFragments()); + destination->setRetry(source->getRetry()); + destination->setPowerMgmt(source->getPowerMgmt()); + destination->setMoreData(source->getMoreData()); + destination->setProtectedFrame(source->getProtectedFrame()); + destination->setOrder(source->getOrder()); + destination->setDurationField(source->getDurationField()); + destination->setAID(source->getAID()); + destination->setReceiverAddress(source->getReceiverAddress()); + destination->setMACArrive(source->getMACArrive()); + destination->setTransmitterAddress(source->getTransmitterAddress()); + destination->setAddress3(source->getAddress3()); + destination->setFragmentNumber(source->getFragmentNumber()); + destination->setSequenceNumber(source->getSequenceNumber()); + return destination; +} + +} // namespace + Register_Class(Fragmentation); std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::vector& fragmentSizes) @@ -21,6 +50,21 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve std::vector *fragments = new std::vector(); const auto& frameHeader = frame->popAtFront(); frame->popAtBack(B(4)); + const auto& actionFrame = dynamicPtrCast(frameHeader); + if (actionFrame != nullptr) { + // IEEE Std 802.11-2024, 10.4: a fragment frame body carries only a + // portion of the MMPDU. Move the action body out of INET's combined + // typed header before slicing it into fragment bodies. + Packet serializedHeader("serializedActionHeader", frameHeader); + const auto& headerBytes = serializedHeader.peekDataAsBytes()->getBytes(); + auto bodyOffset = makeShared()->getChunkLength().get(); + frame->insertAtFront(makeShared(std::vector(headerBytes.begin() + bodyOffset, headerBytes.end()))); + } + B totalFragmentBodyLength = B(0); + for (auto fragmentSize : fragmentSizes) + totalFragmentBodyLength += B(fragmentSize); + if (totalFragmentBodyLength != frame->getDataLength()) + throw cRuntimeError("Fragment sizes total %s but frame body length is %s", totalFragmentBodyLength.str().c_str(), frame->getDataLength().str().c_str()); for (size_t i = 0; i < fragmentSizes.size(); i++) { bool lastFragment = i == fragmentSizes.size() - 1; std::string name = std::string(frame->getName()) + "-frag" + std::to_string(i); @@ -30,7 +74,16 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve fragment->copyTags(*frame); fragment->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset() + offset, fragment->getFrontOffset(), length); offset += length; - const auto& fragmentHeader = staticPtrCast(frameHeader->dupShared()); + Ptr fragmentHeader; + if (actionFrame != nullptr) { + fragmentHeader = copyManagementHeader(actionFrame); + auto actionContext = staticPtrCast(actionFrame->dupShared()); + actionContext->setFragmentNumber(i); + actionContext->setMoreFragments(!lastFragment); + fragment->addTag()->setActionFrame(actionContext); + } + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); fragmentHeader->setSequenceNumber(frameHeader->getSequenceNumber()); fragmentHeader->setFragmentNumber(i); fragmentHeader->setMoreFragments(!lastFragment); diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc new file mode 100644 index 00000000000..1256c20cb64 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc @@ -0,0 +1,15 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" + +namespace inet { +namespace ieee80211 { + +Register_Class(Ieee80211FragmentedActionContextTag); + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h new file mode 100644 index 00000000000..4c853dc3f2f --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h @@ -0,0 +1,48 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IEEE80211FRAGMENTEDACTIONCONTEXTTAG_H +#define __INET_IEEE80211FRAGMENTEDACTIONCONTEXTTAG_H + +#include "inet/common/TagBase.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +namespace inet { +namespace ieee80211 { + +/** + * Sender-local action context for an MPDU fragment whose packet content + * contains only the common management header and its action-body slice. + */ +class INET_API Ieee80211FragmentedActionContextTag : public TagBase +{ + protected: + Ptr actionFrame; + + public: + Ieee80211FragmentedActionContextTag() {} + Ieee80211FragmentedActionContextTag(const Ieee80211FragmentedActionContextTag& other) : TagBase(other), actionFrame(other.actionFrame) {} + + virtual Ieee80211FragmentedActionContextTag *dup() const override { return new Ieee80211FragmentedActionContextTag(*this); } + + const Ptr& getActionFrame() const { return actionFrame; } + void setActionFrame(const Ptr& actionFrame) { this->actionFrame = actionFrame; } +}; + +template +const Ptr findFragmentedActionContext(const Packet *packet) +{ + if (auto contextTag = packet->findTag()) + return dynamicPtrCast(contextTag->getActionFrame()); + else + return dynamicPtrCast(packet->peekAtFront()); +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index 5075d391e89..9d2f17424ce 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -19,12 +19,42 @@ Define_Module(RecipientMacDataService); void RecipientMacDataService::initialize() { duplicateRemoval = new LegacyDuplicateRemoval(); - basicReassembly = new BasicReassembly(); + basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + reassemblyTimer = new cMessage("reassemblyTimer"); +} + +void RecipientMacDataService::handleMessage(cMessage *message) +{ + if (message != reassemblyTimer) + throw cRuntimeError("Unknown message"); + expireReassemblyFragments(); + scheduleReassemblyTimer(); +} + +void RecipientMacDataService::expireReassemblyFragments() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } +} + +void RecipientMacDataService::scheduleReassemblyTimer() +{ + if (reassemblyTimer->isScheduled()) + cancelEvent(reassemblyTimer); + auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, reassemblyTimer); } Packet *RecipientMacDataService::defragment(Packet *dataOrMgmtFrame) { + expireReassemblyFragments(); Packet *packet = basicReassembly->addFragment(dataOrMgmtFrame); + scheduleReassemblyTimer(); if (packet && packet->peekAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -72,6 +102,7 @@ std::vector RecipientMacDataService::controlFrameReceived(Packet *cont RecipientMacDataService::~RecipientMacDataService() { + cancelAndDelete(reassemblyTimer); delete duplicateRemoval; delete basicReassembly; } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h index 63fa2007877..6b696245094 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h @@ -27,6 +27,7 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM protected: IReassembly *basicReassembly = nullptr; // FIXME use Defragmentation + cMessage *reassemblyTimer = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; // Address1Filtering *address1Filtering = nullptr; @@ -37,6 +38,9 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM protected: virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReassemblyFragments(); + virtual void scheduleReassemblyTimer(); virtual Packet *defragment(Packet *dataOrMgmtFrame); virtual std::vector dataOrMgmtFrameReceived(Packet *packet, const Ptr& header); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned index eff3279ebca..3d6ae2e5e37 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned @@ -19,6 +19,7 @@ module RecipientMacDataService extends Module { parameters: @class(RecipientMacDataService); + double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index d6d6302d70a..2bec4629c43 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -24,10 +24,38 @@ Define_Module(RecipientQosMacDataService); void RecipientQosMacDataService::initialize() { duplicateRemoval = new QoSDuplicateRemoval(); - basicReassembly = new BasicReassembly(); + basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); blockAckReordering = new BlockAckReordering(); + reassemblyTimer = new cMessage("reassemblyTimer"); +} + +void RecipientQosMacDataService::handleMessage(cMessage *message) +{ + if (message != reassemblyTimer) + throw cRuntimeError("Unknown message"); + expireReassemblyFragments(); + scheduleReassemblyTimer(); +} + +void RecipientQosMacDataService::expireReassemblyFragments() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } +} + +void RecipientQosMacDataService::scheduleReassemblyTimer() +{ + if (reassemblyTimer->isScheduled()) + cancelEvent(reassemblyTimer); + auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, reassemblyTimer); } void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) @@ -47,19 +75,26 @@ void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress ori Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { + expireReassemblyFragments(); + Packet *defragmentedPacket = nullptr; for (auto fragment : completeFragments) { auto packet = basicReassembly->addFragment(fragment); if (packet != nullptr) { - emit(packetDefragmentedSignal, packet); - return packet; + defragmentedPacket = packet; + break; } } - return nullptr; + scheduleReassemblyTimer(); + if (defragmentedPacket != nullptr) + emit(packetDefragmentedSignal, defragmentedPacket); + return defragmentedPacket; } Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { + expireReassemblyFragments(); auto packet = basicReassembly->addFragment(mgmtFragment); + scheduleReassemblyTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -134,18 +169,27 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { delete mgmtPacket; - return { {}, true }; + // A duplicate fragment is acknowledged by HCF but is not a complete + // MMPDU. Preserve the existing subtype-specific handling only for an + // unfragmented duplicate management frame. + if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) + return { {}, mgmtHeader, true }; + else + return { {}, nullptr, true }; } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } + if (mgmtPacket == nullptr) + return {}; + const auto& completeHeader = mgmtPacket->peekAtFront(); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting - if (dynamicPtrCast(mgmtHeader)) { + if (dynamicPtrCast(completeHeader)) { delete mgmtPacket; - return { {}, false }; + return { {}, completeHeader, false }; } else - return { { mgmtPacket }, false }; + return { { mgmtPacket }, completeHeader, false }; } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) @@ -202,6 +246,7 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c RecipientQosMacDataService::~RecipientQosMacDataService() { + cancelAndDelete(reassemblyTimer); delete duplicateRemoval; delete basicReassembly; delete aMsduDeaggregation; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index 1bc573e4788..644bba53f1a 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,6 +27,7 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; + cMessage *reassemblyTimer = nullptr; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -41,7 +42,10 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, protected: virtual ~RecipientQosMacDataService(); virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReassemblyFragments(); + virtual void scheduleReassemblyTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index 307fb523210..b1ba0021f7e 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned @@ -22,6 +22,7 @@ module RecipientQosMacDataService extends Module { parameters: @class(RecipientQosMacDataService); + double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 49932b3bda9..fcf1f39d75e 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -6,8 +6,8 @@ /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping1 -r 0, 1000s, 8668-d4e7/tplx;e2b1-205b/~tNl;4083-075c/~tND;e1f1-f93c/tyf, PASS, wireless adhoc Ipv4 # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 1486-cbae/tplx;521b-977a/~tNl;f7ee-50fd/~tND;d6e9-44b3/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, bbb9-ef75/tplx;b738-d1b7/~tNl;47b1-07f1/~tND;c7de-0741/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, ca86-69c6/tplx;521b-977a/~tNl;f7ee-50fd/~tND;5471-336a/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, f8a7-3cf6/tplx;b738-d1b7/~tNl;47b1-07f1/~tND;0635-98e5/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 9c66-e6f0/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 85bd-85c6/tplx;22f6-085d/~tNl;e4ea-962c/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -655,9 +655,9 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;7a95-cf35/~tND, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, c3c0-dc93/tplx;5eb9-6cea/~tNl;87a5-11a1/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 3a58-19c0/tplx;6188-6ffd/~tNl;377c-d4fc/~tND;7647-5f80/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, c896-d56c/tplx;0427-432c/~tNl;7327-574f/~tND;ae9f-1da4/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 33e5-c380/tplx;78d2-bb9a/~tNl;1524-d88f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 7f3b-72ca/tplx;7c57-849b/~tNl;06e3-7901/~tND;3d84-93de/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, d637-2202/tplx;a6ab-03ac/~tNl;9f12-2283/~tND;22b8-1e1c/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 5570-9d59/tplx;c521-a55d/~tNl;e5fc-a44c/~tND;e59c-a8ed/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, 4221-b2c3/tplx;f9c2-e9ef/~tNl;5ab0-68ce/~tND, PASS, wireless Ipv4 diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index d611f9ff408..975eb111f8d 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -266,7 +266,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 57ee-7ddf/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, f7a5-cf0a/tplx;a24a-a070/~tNl;5dae-0b8e/~tND;11a9-72e7/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, 4b00-315e/tplx;a24a-a070/~tNl;5dae-0b8e/~tND;70a1-3cc7/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 892b-8baa/tplx;370a-d694/~tNl;ab8b-0a8a/~tND;641e-b548/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 3623-0ff6/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -314,7 +314,7 @@ /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/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 26be-ff05/tplx;9c39-00a3/~tNl;03a9-2f02/~tND;5fef-ddb3/tyf, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 8d7c-5091/tplx;84a6-b893/~tNl;cb18-5b4d/~tND;c7c6-29c8/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, faed-a002/tplx;84a6-b893/~tNl;cb18-5b4d/~tND;6132-2b2c/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/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 9e3a5a153a3..72e7714ddf5 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -25,13 +25,17 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -84,6 +88,15 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; +class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosAckPolicy +{ + public: + virtual bool isAckNeeded(const Ptr&) const override { return true; } + virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *) const override { return false; } + virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } + virtual simtime_t computeBasicBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } +}; + class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback { public: @@ -568,6 +581,7 @@ class TestHcf : public Hcf int numResumedEligibleChannelAccess = 0; int numCancelledAddbaTransactions = 0; int numRebuildEligibilityCalls = 0; + int numTransmittedControlResponses = 0; bool droppedSetupCancelled = false; bool delegateDroppedSetupHandling = false; TestCallback *managementCallback = nullptr; @@ -593,6 +607,10 @@ class TestHcf : public Hcf } void configureFrameSequenceHandler(IFrameSequenceHandler *frameSequenceHandler) { this->frameSequenceHandler = frameSequenceHandler; } void configureRecipientDataService(IRecipientQosMacDataService *recipientDataService) { this->recipientDataService = recipientDataService; } + void configureRecipientAck(IRecipientQosAckPolicy *recipientAckPolicy) { + this->recipientAckPolicy = recipientAckPolicy; + recipientAckProcedure = new RecipientAckProcedure(); + } void configureManagementCallback(TestCallback *managementCallback) { this->managementCallback = managementCallback; } void configureTransmittedData(Edca *edca, IOriginatorBlockAckAgreementHandler *handler, IOriginatorBlockAckAgreementPolicy *policy) { this->edca = edca; @@ -616,6 +634,7 @@ class TestHcf : public Hcf void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } + void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { @@ -625,6 +644,10 @@ class TestHcf : public Hcf } protected: + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numTransmittedControlResponses++; + delete responsePacket; + } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { if (managementCallback != nullptr) managementCallback->processMgmtFrame(packet, header); @@ -667,7 +690,7 @@ class TestEmptyFrameSequence : public IFrameSequence class TestFragmentationPolicy : public IFragmentationPolicy { public: - virtual std::vector computeFragmentSizes(Packet *) override { return { 2, 2 }; } + virtual std::vector computeFragmentSizes(Packet *) override { return { 8, 5 }; } }; class TestMsduAggregationPolicy : public IMsduAggregationPolicy @@ -995,7 +1018,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1043,7 +1066,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1063,19 +1086,33 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); hcf.configureTransmittedData(&edca, originatorHandler, &policy); hcf.configureTransmittedManagement(&edca, &ackPolicy); - auto sibling = makeTaggedDelbaPacket("acknowledgedDelbaSibling", peer1, 6, SequenceNumberCyclic(204), 0, true, transactionId); - auto finalPacket = makeTaggedDelbaPacket("acknowledgedFinalDelba", peer1, 6, SequenceNumberCyclic(204), 1, false, transactionId); - auto siblingHeader = sibling->peekAtFront(); - auto finalHeader = finalPacket->peekAtFront(); + auto completeDelba = makeShared(); + completeDelba->setReceiverAddress(peer1); + completeDelba->setTid(6); + completeDelba->setInitiator(true); + completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); + auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); + completeDelbaPacket->addTag()->setTransactionId(transactionId); + completeDelbaPacket->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto delbaFragments = fragmentation.fragmentFrame(completeDelbaPacket, { 4, 2 }); + auto sibling = delbaFragments->at(0); + auto finalPacket = delbaFragments->at(1); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + ASSERT(dynamicPtrCast(siblingHeader) == nullptr); + ASSERT(dynamicPtrCast(finalHeader) == nullptr); originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); inProgressFrames.addOwnedFrame(sibling); inProgressFrames.addOwnedFrame(finalPacket); ackHandler.frameGotInProgress(siblingHeader); ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(sibling, siblingHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(sibling, findFragmentedActionContext(sibling))); hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); Packet ackPacket("transactionalDelbaAck", makeShared()); hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); - ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, findFragmentedActionContext(finalPacket))); ASSERT(hcf.numCancelledAddbaTransactions == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); @@ -1086,6 +1123,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; + delete delbaFragments; } // Reaching the retry limit aborts the teardown through HCF, clears its sibling @@ -1100,7 +1138,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1482,8 +1520,17 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto transmittedDelba = makeShared(); transmittedDelba->setReceiverAddress(peer2); transmittedDelba->setTid(2); + transmittedDelba->setInitiator(true); + transmittedDelba->setMoreFragments(true); + auto transmittedFinalDelba = staticPtrCast(transmittedDelba->dupShared()); + transmittedFinalDelba->setMoreFragments(false); + transmittedFinalDelba->setFragmentNumber(1); Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); - auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback); + ASSERT(transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback) == nullptr); + ASSERT(transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr); + ASSERT(transmittedDelbaCallback.cancelledTransactionIds.empty()); + Packet transmittedFinalDelbaPacket("transmittedFinalDelba", transmittedFinalDelba); + auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedFinalDelbaPacket, &transmittedDelbaCallback); ASSERT(terminatedPendingAgreement != nullptr); ASSERT(terminatedPendingAgreement->isPending()); if (transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr || transmittedDelbaCallback.cancelledTransactionIds.size() != 1 || transmittedDelbaCallback.cancelledTransactionIds.back() != transmittedDelbaTransactionId) @@ -1494,18 +1541,21 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(transmittedDelbaCallback.addbaDeadline == SIMTIME_MAX); } -// Fragmentation preserves the local transaction identity on every MPDU, and -// typed cancellation removes all exact siblings while retaining the dropped -// fragment and an unrelated transaction. +// Fragmentation preserves the local transaction identity on every MPDU. +// Transaction-scoped cancellation removes all exact siblings while retaining +// the dropped fragment and an unrelated transaction. { - TestOriginatorHandler fragmentedHandler; + auto fragmentedHandler = new TestOriginatorHandler(); TestCallback fragmentedCallback; Packet fragmentedTrigger("fragmentedTrigger"); auto fragmentedTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(50)); - fragmentedHandler.processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); + fragmentedHandler->processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); auto unfragmentedRequestPacket = fragmentedCallback.managementPacket; auto fragmentedTransactionId = unfragmentedRequestPacket->getTag()->getTransactionId(); fragmentedCallback.forgetManagementPacket(unfragmentedRequestPacket); + auto unfragmentedRequestHeader = unfragmentedRequestPacket->removeAtFront(); + unfragmentedRequestHeader->setSequenceNumber(SequenceNumberCyclic(50)); + unfragmentedRequestPacket->insertAtFront(unfragmentedRequestHeader); unfragmentedRequestPacket->insertAtBack(makeShared(B(4))); unfragmentedRequestPacket->insertAtBack(makeShared()); TestFragmentationPolicy fragmentationPolicy; @@ -1514,8 +1564,11 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto fragments = fragmentationDataService.fragment(unfragmentedRequestPacket); ASSERT(fragments != nullptr); ASSERT(fragments->size() == 2); - for (auto fragment : *fragments) + for (auto fragment : *fragments) { ASSERT(fragment->getTag()->getTransactionId() == fragmentedTransactionId); + ASSERT(dynamicPtrCast(fragment->peekAtFront()) == nullptr); + ASSERT(findFragmentedActionContext(fragment) != nullptr); + } auto unrelatedPacket = new Packet("unrelatedTransaction", makeShared(B(1))); unrelatedPacket->addTag()->setTransactionId(fragmentedTransactionId + 1); auto droppedFragment = fragments->at(0); @@ -1524,15 +1577,30 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); fragmentedPendingQueue.packets = { droppedFragment, siblingFragment, unrelatedPacket }; TestInProgressFrames fragmentedInProgressFrames; TestEdcaf fragmentedEdcaf; + TestQosAckHandler fragmentedAckHandler; fragmentedEdcaf.pendingQueue = &fragmentedPendingQueue; fragmentedEdcaf.inProgressFrames = &fragmentedInProgressFrames; + fragmentedEdcaf.qosAckHandler = &fragmentedAckHandler; TestEdca fragmentedEdca; fragmentedEdca.edcaf = &fragmentedEdcaf; TestHcf fragmentedHcf; fragmentedHcf.configure(&fragmentedEdca); - auto droppedRequest = droppedFragment->peekAtFront(); - fragmentedHandler.processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); - ASSERT(fragmentedHandler.getAgreement(peer1, 7) == nullptr); + OriginatorQosAckPolicy fragmentedAckPolicy; + TestRecipientPolicy fragmentedRecipientPolicy; + fragmentedHcf.configureBlockAckHandlers(fragmentedHandler, &originatorPolicy, new TestRecipientHandler(), &fragmentedRecipientPolicy); + fragmentedHcf.configureTransmittedManagement(&fragmentedEdca, &fragmentedAckPolicy); + auto fragmentedAgreement = fragmentedHandler->getAgreement(peer1, 7); + ASSERT(fragmentedAgreement != nullptr); + ASSERT(!fragmentedAgreement->getIsAddbaRequestSent()); + auto firstFragmentHeader = droppedFragment->peekAtFront(); + auto finalFragmentHeader = siblingFragment->peekAtFront(); + fragmentedHcf.processTransmittedManagement(droppedFragment, firstFragmentHeader, AC_BE); + ASSERT(!fragmentedAgreement->getIsAddbaRequestSent()); + fragmentedHcf.processTransmittedManagement(siblingFragment, finalFragmentHeader, AC_BE); + ASSERT(fragmentedAgreement->getIsAddbaRequestSent()); + auto droppedRequest = findFragmentedActionContext(droppedFragment); + fragmentedHandler->processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); + ASSERT(fragmentedHandler->getAgreement(peer1, 7) == nullptr); ASSERT(fragmentedPendingQueue.getNumPackets() == 2); ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), droppedFragment) != fragmentedPendingQueue.packets.end()); ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), siblingFragment) == fragmentedPendingQueue.packets.end()); @@ -1583,7 +1651,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); rejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("initiallyRejectedAddbaRequest", rejectedRequest), rejectedRequest); ASSERT(!rejectedReception.duplicate); - hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, rejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(7, peer3) == nullptr); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); ASSERT(responseCallback.managementPackets.size() == 1); @@ -1594,7 +1663,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); duplicateRejectedRequest->setRetry(true); auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateInitiallyRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); ASSERT(duplicateReception.duplicate); - hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, duplicateReception.duplicate); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); ASSERT(responseCallback.managementPackets.size() == 1); @@ -1633,7 +1703,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); ASSERT(!firstReception.duplicate); ASSERT(firstReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(firstRequest, firstReception.duplicate); + ASSERT(firstReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(firstReception.completeHeader, firstReception.duplicate); auto firstAgreement = recipientHandler->getAgreement(6, peer2); ASSERT(firstAgreement != nullptr); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); @@ -1670,7 +1741,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateAddbaRequest", duplicateRequest), duplicateRequest); ASSERT(duplicateReception.duplicate); ASSERT(duplicateReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(duplicateRequest, duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, duplicateReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(firstAgreement->getBlockAckRecord() == firstBlockAckRecord); ASSERT(firstAgreement->getExpirationTime() == firstExpirationTime); @@ -1695,7 +1767,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); rejectedRequest->setBlockAckTimeoutValue(4); auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("rejectedAddbaRequest", rejectedRequest), rejectedRequest); ASSERT(!rejectedReception.duplicate); - hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, rejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(changedListener.numSignals == 0); ASSERT(reorderDropListener.numSignals == 0); @@ -1715,7 +1788,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); duplicateRejectedRequest->setBlockAckTimeoutValue(4); auto duplicateRejectedReception = recipientDataService->managementFrameReceived(new Packet("duplicateRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); ASSERT(duplicateRejectedReception.duplicate); - hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateRejectedReception.duplicate); + ASSERT(duplicateRejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateRejectedReception.completeHeader, duplicateRejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); ASSERT(changedListener.numSignals == 0); @@ -1766,7 +1840,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto replacementReception = recipientDataService->managementFrameReceived(new Packet("replacementAddbaRequest", replacementRequest), replacementRequest); ASSERT(!replacementReception.duplicate); ASSERT(replacementReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(replacementRequest, replacementReception.duplicate); + ASSERT(replacementReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(replacementReception.completeHeader, replacementReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) != firstAgreement); ASSERT(addedListener.numSignals == 1); ASSERT(changedListener.numSignals == 1); @@ -1802,6 +1877,278 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); recipientDataServiceModule->deleteModule(); } +// IEEE Std 802.11-2024, 10.4-10.5: each management MPDU fragment is +// acknowledged independently, but its action body is dispatched only after +// the final fragment and every preceding fragment have been received. +{ + auto fragmentationPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("addbaFragmentationPolicy", this); + fragmentationPolicyModule->par("fragmentationThreshold").setIntValue(34); + fragmentationPolicyModule->callInitialize(); + auto fragmentationPolicy = check_and_cast(fragmentationPolicyModule); + auto policyRequest = makeRequest(peer2, 2, 72, SequenceNumberCyclic(102), 32); + auto policyPacket = new Packet("policyAddbaRequest", policyRequest); + policyPacket->insertAtBack(makeShared()); + ASSERT(fragmentationPolicy->computeFragmentSizes(policyPacket) == std::vector({ 6, 3 })); + delete policyPacket; + auto groupAddressedRequest = makeRequest(peer2, 2, 72, SequenceNumberCyclic(102), 32); + groupAddressedRequest->setReceiverAddress(MacAddress::BROADCAST_ADDRESS); + auto groupAddressedPacket = new Packet("groupAddressedAddbaRequest", groupAddressedRequest); + groupAddressedPacket->insertAtBack(makeShared()); + ASSERT(fragmentationPolicy->computeFragmentSizes(groupAddressedPacket).empty()); + delete groupAddressedPacket; + fragmentationPolicyModule->callFinish(); + fragmentationPolicyModule->deleteModule(); + + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("fragmentedAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy fragmentedPolicy; + TestCallback fragmentedCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &fragmentedPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&fragmentedCallback); + + auto makeFragments = [&](const char *name, Tid tid, int sequenceNumber) { + auto request = makeRequest(peer2, tid, 70 + tid, SequenceNumberCyclic(100 + tid), 32); + request->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto packet = new Packet(name, request); + packet->insertAtBack(makeShared(B(4))); + packet->insertAtBack(makeShared()); + Fragmentation fragmentation; + return fragmentation.fragmentFrame(packet, { 8, 5 }); + }; + auto receiveFragment = [&](Packet *fragment) { + auto header = fragment->peekAtFront(); + return recipientDataService->managementFrameReceived(fragment, header); + }; + auto convertToWireRepresentation = [](Packet *fragment) { + std::string name = fragment->getName(); + const auto& trailer = fragment->popAtBack(B(4)); + auto wireFragment = new Packet(name.c_str(), fragment->peekDataAsBytes()); + wireFragment->copyTags(*fragment); + wireFragment->insertAtBack(trailer); + delete fragment; + return wireFragment; + }; + + auto fragments = makeFragments("fragmentedAddbaRequest", 3, 60); + for (auto& fragment : *fragments) + fragment = convertToWireRepresentation(fragment); + auto commonManagementHeaderLength = makeShared()->getChunkLength(); + ASSERT(fragments->at(0)->getByteLength() == commonManagementHeaderLength.get() + 8 + 4); + ASSERT(fragments->at(1)->getByteLength() == commonManagementHeaderLength.get() + 5 + 4); + ASSERT(dynamicPtrCast(fragments->at(0)->peekAtFront()) == nullptr); + ASSERT(dynamicPtrCast(fragments->at(1)->peekAtFront()) == nullptr); + auto duplicateFirstFragment = fragments->at(0)->dup(); + auto duplicateHeader = duplicateFirstFragment->removeAtFront(); + duplicateHeader->setRetry(true); + duplicateFirstFragment->insertAtFront(duplicateHeader); + + auto firstReception = receiveFragment(fragments->at(0)); + ASSERT(firstReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.empty()); + + auto duplicateReception = receiveFragment(duplicateFirstFragment); + ASSERT(duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.empty()); + + auto completeReception = receiveFragment(fragments->at(1)); + ASSERT(!completeReception.duplicate); + ASSERT(completeReception.completeHeader != nullptr); + ASSERT(completeReception.completeHeader->getFragmentNumber() == 0); + ASSERT(!completeReception.completeHeader->getMoreFragments()); + auto completeRequest = dynamicPtrCast(completeReception.completeHeader); + ASSERT(completeRequest != nullptr); + ASSERT(completeRequest->getTid() == 3); + ASSERT(completeRequest->getDialogToken() == 73); + ASSERT(completeRequest->getStartingSequenceNumber() == SequenceNumberCyclic(103)); + hcf.processReceivedManagementFrame(completeReception.completeHeader); + ASSERT(recipientHandler->getAgreement(3, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 1); + delete fragments; + + // Seeing the final fragment first does not imply completeness. Dispatch + // occurs only when the missing lower-numbered fragment later arrives. + auto outOfOrderFragments = makeFragments("outOfOrderAddbaRequest", 4, 61); + for (auto& fragment : *outOfOrderFragments) + fragment = convertToWireRepresentation(fragment); + auto finalFirstReception = receiveFragment(outOfOrderFragments->at(1)); + ASSERT(finalFirstReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(4, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 1); + auto outOfOrderCompleteReception = receiveFragment(outOfOrderFragments->at(0)); + ASSERT(outOfOrderCompleteReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(outOfOrderCompleteReception.completeHeader); + ASSERT(recipientHandler->getAgreement(4, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 2); + delete outOfOrderFragments; + + // The receive timer releases an incomplete MMPDU at + // dot11MaxReceiveLifetime even if no matching fragment arrives. + { + auto expiringDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("expiringFragmentDataService", this); + expiringDataServiceModule->par("maxReceiveLifetime").setDoubleValue(1e-6); + expiringDataServiceModule->callInitialize(); + auto expiringDataService = check_and_cast(expiringDataServiceModule); + TestPacketDropSignalListener expiryDropListener; + expiringDataService->subscribe(packetDroppedSignal, &expiryDropListener); + auto expiredFragments = makeFragments("expiredAddbaRequest", 1, 66); + auto firstExpiredReception = expiringDataService->managementFrameReceived(expiredFragments->at(0), expiredFragments->at(0)->peekAtFront()); + ASSERT(firstExpiredReception.completeHeader == nullptr); + ASSERT(expiryDropListener.numOtherPacketDrops == 0); + wait(2e-6); + ASSERT(expiryDropListener.numOtherPacketDrops == 1); + auto lateFinalReception = expiringDataService->managementFrameReceived(expiredFragments->at(1), expiredFragments->at(1)->peekAtFront()); + ASSERT(lateFinalReception.completeHeader == nullptr); + // The late final fragment must be discarded, not buffered as the + // beginning of another reassembly. A subsequent non-Retry fragment 0 + // starts a new sequence reuse and therefore cannot combine with it. + auto restartedFragments = makeFragments("restartedAddbaRequest", 1, 66); + auto restartedReception = expiringDataService->managementFrameReceived(restartedFragments->at(0), restartedFragments->at(0)->peekAtFront()); + ASSERT(restartedReception.completeHeader == nullptr); + delete restartedFragments->at(1); + delete restartedFragments; + expiringDataServiceModule->callFinish(); + expiringDataServiceModule->deleteModule(); + delete expiredFragments; + } + + // Frame type and destination address are part of the reassembly identity; + // fragments sharing only TA and sequence number cannot cross-associate. + { + BasicReassembly isolatedReassembly; + auto firstMmpdu = makeFragments("isolatedAddbaRequest", 0, 67); + auto otherDestinationMmpdu = makeFragments("otherDestinationAddbaRequest", 0, 67); + auto setReceiverAddress = [](Packet *fragment, const MacAddress& receiverAddress) { + auto header = fragment->removeAtFront(); + header->setReceiverAddress(receiverAddress); + fragment->insertAtFront(header); + }; + for (auto fragment : *firstMmpdu) + setReceiverAddress(fragment, peer1); + for (auto fragment : *otherDestinationMmpdu) + setReceiverAddress(fragment, peer3); + auto dataHeader = makeShared(); + dataHeader->setType(ST_DATA); + dataHeader->setTransmitterAddress(peer2); + dataHeader->setReceiverAddress(peer1); + dataHeader->setSequenceNumber(SequenceNumberCyclic(67)); + dataHeader->setFragmentNumber(1); + dataHeader->setMoreFragments(false); + auto dataFragment = new Packet("sameSequenceDataFragment", dataHeader); + dataFragment->insertAtBack(makeShared(B(5))); + dataFragment->insertAtBack(makeShared()); + ASSERT(isolatedReassembly.addFragment(firstMmpdu->at(0)) == nullptr); + ASSERT(isolatedReassembly.addFragment(otherDestinationMmpdu->at(1)) == nullptr); + ASSERT(isolatedReassembly.addFragment(dataFragment) == nullptr); + auto completeMmpdu = isolatedReassembly.addFragment(firstMmpdu->at(1)); + ASSERT(completeMmpdu != nullptr); + delete completeMmpdu; + delete otherDestinationMmpdu->at(0); + delete firstMmpdu; + delete otherDestinationMmpdu; + } + + // Drive the real HCF recipient entry point: every MPDU, including an + // ignored duplicate, is ACKed before the data service gates action-body + // dispatch on complete reassembly. + TestRecipientAckPolicy recipientAckPolicy; + hcf.configureRecipientAck(&recipientAckPolicy); + auto hcfFragments = makeFragments("hcfFragmentedAddbaRequest", 7, 65); + for (auto& fragment : *hcfFragments) + fragment = convertToWireRepresentation(fragment); + auto hcfDuplicateFirstFragment = hcfFragments->at(0)->dup(); + auto hcfDuplicateHeader = hcfDuplicateFirstFragment->removeAtFront(); + hcfDuplicateHeader->setRetry(true); + hcfDuplicateFirstFragment->insertAtFront(hcfDuplicateHeader); + auto numManagementPacketsBeforeHcfReception = fragmentedCallback.managementPackets.size(); + hcf.processReceivedFrame(hcfFragments->at(0)); + ASSERT(hcf.numTransmittedControlResponses == 1); + ASSERT(recipientHandler->getAgreement(7, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception); + hcf.processReceivedFrame(hcfDuplicateFirstFragment); + ASSERT(hcf.numTransmittedControlResponses == 2); + ASSERT(recipientHandler->getAgreement(7, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception); + hcf.processReceivedFrame(hcfFragments->at(1)); + ASSERT(hcf.numTransmittedControlResponses == 3); + ASSERT(recipientHandler->getAgreement(7, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception + 1); + delete hcfFragments; + + auto verifyFragmentedActionRoundTrip = [&](const char *name, const Ptr& action, const std::function&)>& verify) { + auto bodyLength = (int)(action->getChunkLength() - commonManagementHeaderLength).get(); + auto firstFragmentLength = (bodyLength - 1) & ~1; + ASSERT(firstFragmentLength > 0 && firstFragmentLength < bodyLength); + auto packet = new Packet(name, action); + packet->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto actionFragments = fragmentation.fragmentFrame(packet, { firstFragmentLength, bodyLength - firstFragmentLength }); + for (auto& fragment : *actionFragments) { + ASSERT(dynamicPtrCast(fragment->peekAtFront()) == nullptr); + ASSERT(fragment->getTag()->getActionFrame() != nullptr); + fragment = convertToWireRepresentation(fragment); + } + auto incompleteReception = receiveFragment(actionFragments->at(0)); + ASSERT(incompleteReception.completeHeader == nullptr); + auto completedReception = receiveFragment(actionFragments->at(1)); + ASSERT(completedReception.completeHeader != nullptr); + auto completedAction = dynamicPtrCast(completedReception.completeHeader); + ASSERT(completedAction != nullptr); + ASSERT(completedAction->getReceiverAddress() == action->getReceiverAddress()); + ASSERT(completedAction->getTransmitterAddress() == action->getTransmitterAddress()); + ASSERT(completedAction->getAddress3() == action->getAddress3()); + ASSERT(completedAction->getSequenceNumber() == action->getSequenceNumber()); + verify(completedAction); + delete actionFragments; + return completedAction; + }; + + auto fragmentedResponse = makeResponse(peer2, 5, 81, 0); + fragmentedResponse->setSequenceNumber(SequenceNumberCyclic(62)); + verifyFragmentedActionRoundTrip("fragmentedAddbaResponse", fragmentedResponse, [](const Ptr& action) { + auto response = dynamicPtrCast(action); + ASSERT(response != nullptr); + ASSERT(response->getTid() == 5); + ASSERT(response->getDialogToken() == 81); + ASSERT(response->getStatusCode() == 0); + }); + + for (bool initiator : { false, true }) { + if (initiator) { + auto delbaAgreementRequest = makeRequest(peer2, 6, 86, SequenceNumberCyclic(106), 32); + ASSERT(recipientHandler->processReceivedAddbaRequest(delbaAgreementRequest, &fragmentedPolicy, &fragmentedCallback, &fragmentedCallback) != nullptr); + ASSERT(recipientHandler->getAgreement(6, peer2) != nullptr); + } + auto fragmentedDelba = makeShared(); + fragmentedDelba->setTransmitterAddress(peer2); + fragmentedDelba->setSequenceNumber(SequenceNumberCyclic(initiator ? 64 : 63)); + fragmentedDelba->setInitiator(initiator); + fragmentedDelba->setTid(6); + fragmentedDelba->setReasonCode(39); + auto completeDelba = verifyFragmentedActionRoundTrip(initiator ? "fragmentedOriginatorDelba" : "fragmentedRecipientDelba", fragmentedDelba, [initiator](const Ptr& action) { + auto delba = dynamicPtrCast(action); + ASSERT(delba != nullptr); + ASSERT(delba->getInitiator() == initiator); + ASSERT(delba->getTid() == 6); + ASSERT(delba->getReasonCode() == 39); + }); + if (initiator) { + hcf.processReceivedManagementFrame(completeDelba); + ASSERT(recipientHandler->getAgreement(6, peer2) == nullptr); + } + } + + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + // IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. RecipientBlockAckAgreementHandler immediateRecipientHandler; TestRecipientPolicy recipientPolicy; @@ -2010,8 +2357,17 @@ ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateA delba->setTid(3); delba->setInitiator(false); delba->setSequenceNumber(SequenceNumberCyclic(200)); + delba->setMoreFragments(true); + auto finalDelba = staticPtrCast(delba->dupShared()); + finalDelba->setMoreFragments(false); + finalDelba->setFragmentNumber(1); Packet delbaPacket("transmittedRecipientDelba", delba); hcf.processTransmittedManagement(&delbaPacket, delba, AC_BE); + ASSERT(recipientHandler->getAgreement(3, peer2) != nullptr); + ASSERT(deletionListener.numSignals == 0); + ASSERT(reorderDropListener.numOtherPacketDrops == 0); + Packet finalDelbaPacket("transmittedRecipientDelbaFinal", finalDelba); + hcf.processTransmittedManagement(&finalDelbaPacket, finalDelba, AC_BE); ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); ASSERT(deletionListener.numSignals == 1); ASSERT(reorderDropListener.numOtherPacketDrops == 1); From ad8a9e2529884de7ac85df963f847655840d0f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:42:05 +0200 Subject: [PATCH 32/91] ieee80211: add+fix: cancel superseded management transactions A superseded AP management transaction must not leave queued siblings or continue a later frame exchange. Distinguish deferred cancellation from immediate abort so an already handed-down transmission keeps its owner until completion, and require explicit sequence-handler implementations. Validation: diagnostic debug and release builds; 10 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211 | behavior.add+change.fix | test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 2 + doc/src/migration-guide/index.rst | 10 + .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 9 + .../linklayer/ieee80211/mac/Ieee80211Mac.h | 6 +- src/inet/linklayer/ieee80211/mac/Tx.cc | 12 + src/inet/linklayer/ieee80211/mac/Tx.h | 1 + .../mac/contract/IFrameSequenceHandler.h | 15 + .../IManagementFrameTransactionHandler.h | 37 + .../linklayer/ieee80211/mac/contract/ITx.h | 7 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 180 ++++- .../ieee80211/mac/coordinationfunction/Dcf.h | 20 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 169 ++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 17 + .../mac/framesequence/FrameSequenceHandler.cc | 19 +- .../mac/framesequence/FrameSequenceHandler.h | 4 +- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 20 +- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 3 + .../module/Ieee80211MgmtApCancellation_1.test | 656 ++++++++++++++++++ .../module/Ieee80211MgmtApHcfQueueDrop_1.test | 58 ++ tests/module/Ieee80211MgmtApQueueDrop_1.test | 58 ++ tests/unit/Ieee80211AddbaTransaction_1.test | 450 +++++++++++- 21 files changed, 1733 insertions(+), 20 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h create mode 100644 tests/module/Ieee80211MgmtApCancellation_1.test diff --git a/WHATSNEW b/WHATSNEW index d74f2291414..a431aff0e42 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -337,6 +337,8 @@ Notable backward incompatible changes are the following: Custom originator agreement handlers must explicitly implement isDelbaPending(). + Custom IFrameSequenceHandler implementations must explicitly implement + cancelFrameSequence() and abortFrameSequence(). IOriginatorBlockAckAgreementHandler::processReceivedDelba(), IOriginatorBlockAckAgreementHandler::processTransmittedDelba(), and diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 8b097fe5c5c..be2f3d4c376 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -130,6 +130,16 @@ context across fragmented transmission, reconstruct the complete action body, an dispatch its subtype only after reassembly. Fragmented on-air management headers do not carry a complete action body in every fragment. +Management Frame Sequence Cancellation +-------------------------------------- + +Custom ``IFrameSequenceHandler`` implementations must explicitly implement +``cancelFrameSequence()`` and ``abortFrameSequence()``. Preserve the distinction between +requesting deferred cancellation and immediately aborting a sequence. Coordinate packet +ownership with the caller: cancellation must not delete a packet still borrowed by an +in-flight transmission or active sequence. Superseded AP management transactions must +retire their queued siblings and terminal outcome exactly once. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index da8dd51225e..825f1c5b82a 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -441,6 +441,15 @@ void Ieee80211Mac::sendDownPendingRadioConfigMsg() } } +void Ieee80211Mac::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + if (mib->qos) + hcf->cancelManagementTransaction(transactionId); + else + dcf->cancelManagementTransaction(transactionId); +} + void Ieee80211Mac::processUpperFrame(Packet *packet, const Ptr& header) { Enter_Method("processUpperFrame(\"%s\")", packet->getName()); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 09b69439f3a..8b5756a3544 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -16,6 +16,7 @@ #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRx.h" #include "inet/linklayer/ieee80211/mac/contract/ITx.h" +#include "inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Mcf.h" @@ -36,7 +37,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, public IIeee80211MacConfiguration +class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfiguration, public IManagementFrameTransactionHandler { public: static simsignal_t frameTransmissionOutcomeSignal; @@ -111,6 +112,8 @@ class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfig virtual void sendDownFrame(Packet *frame); virtual void sendDownPendingRadioConfigMsg(); + virtual void cancelManagementTransaction(uint64_t transactionId) override; + virtual void processUpperFrame(Packet *packet, const Ptr& header); virtual void processLowerFrame(Packet *packet, const Ptr& header); }; @@ -119,4 +122,3 @@ class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfig } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/Tx.cc b/src/inet/linklayer/ieee80211/mac/Tx.cc index 9c0d906802b..8e634d078a0 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.cc +++ b/src/inet/linklayer/ieee80211/mac/Tx.cc @@ -82,6 +82,18 @@ void Tx::transmitFrame(Packet *packet, const Ptr& head scheduleAfter(ifs, endIfsTimer); } +bool Tx::cancelPendingTransmission(ITx::ICallback *owner) +{ + Enter_Method("cancelPendingTransmission"); + if (this->txCallback != owner || transmitting || frame == nullptr || !endIfsTimer->isScheduled()) + return false; + cancelEvent(endIfsTimer); + delete frame; + frame = nullptr; + txCallback = nullptr; + return true; +} + void Tx::radioTransmissionFinished() { Enter_Method("radioTransmissionFinished"); diff --git a/src/inet/linklayer/ieee80211/mac/Tx.h b/src/inet/linklayer/ieee80211/mac/Tx.h index ae4c37308b6..ed7331f4d08 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.h +++ b/src/inet/linklayer/ieee80211/mac/Tx.h @@ -41,6 +41,7 @@ class INET_API Tx : public SimpleModule, public ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ITx::ICallback *txCallback) override; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ITx::ICallback *txCallback) override; + virtual bool cancelPendingTransmission(ITx::ICallback *owner) override; virtual void radioTransmissionFinished() override; }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index 46f9a820f28..a71e80607c9 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -46,6 +46,21 @@ class INET_API IFrameSequenceHandler virtual void transmissionComplete() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; + + /** + * Requests cancellation at the next ownership-safe frame-sequence + * boundary. An in-flight transmit step remains owned by the sequence + * until its physical transmission completion is delivered. + */ + virtual void cancelFrameSequence() = 0; + + /** + * Aborts a frame sequence immediately after its pending transmission has + * been cancelled before lower-layer handoff. This is separate from + * cancelFrameSequence() because an already handed-down copy must be + * allowed to complete first. + */ + virtual void abortFrameSequence() = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h new file mode 100644 index 00000000000..7b7c806bfef --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h @@ -0,0 +1,37 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IMANAGEMENTFRAMETRANSACTIONHANDLER_H +#define __INET_IMANAGEMENTFRAMETRANSACTIONHANDLER_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace ieee80211 { + +/** + * Provides management-frame transaction lifecycle operations to management + * protocols without exposing coordination-function implementation details. + */ +class INET_API IManagementFrameTransactionHandler +{ + public: + virtual ~IManagementFrameTransactionHandler() {} + + /** + * Cancels all locally queued fragments for the given management + * transaction. An active transmission is retired at an ownership-safe + * frame-sequence boundary. + */ + virtual void cancelManagementTransaction(uint64_t transactionId) = 0; +}; + +} +} + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/ITx.h b/src/inet/linklayer/ieee80211/mac/contract/ITx.h index 090969da65c..a7a81d9980f 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/ITx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/ITx.h @@ -33,6 +33,13 @@ class INET_API ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ICallback *callback) = 0; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ICallback *callback) = 0; + /** + * Cancels a transmission that is still waiting for its inter-frame + * space. The owner must be the callback supplied to transmitFrame(). + * Returns true only when no copy has been handed to the lower layer yet. + * A transmission that is already in progress is left untouched. + */ + virtual bool cancelPendingTransmission(ICallback *owner) = 0; virtual void radioTransmissionFinished() = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index a660a3750f3..5416a21a455 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/framesequence/DcfFs.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" @@ -141,13 +142,22 @@ void Dcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, void Dcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) { Enter_Method("handlePacketRemoved"); - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + auto transactionTag = packet->findTag(); + if ((reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) && transactionTag != nullptr) { + if (cancelManagementTransaction(transactionTag->getTransactionId(), packet)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } +void Dcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); +} + void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) { emit(packetSentToPeerSignal, packet); @@ -159,6 +169,135 @@ void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const throw cRuntimeError("Unknown control response frame"); } +bool Dcf::isPacketReferencedByCurrentFrameSequence(const Packet *packet) const +{ + if (frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto transmitStep = dynamic_cast(context->getStep(i)); + if (transmitStep != nullptr && transmitStep->getFrameToTransmit() == packet) + return true; + auto rtsTransmitStep = dynamic_cast(transmitStep); + if (rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet) + return true; + } + return false; +} + +bool Dcf::isManagementTransactionCancelled(const Packet *packet) const +{ + if (packet == nullptr) + return false; + auto transactionTag = packet->findTag(); + return transactionTag != nullptr && cancelledManagementTransactions.find(transactionTag->getTransactionId()) != cancelledManagementTransactions.end(); +} + +bool Dcf::isCurrentFrameSequenceCancelled(const Packet *packet) const +{ + if (packet == nullptr || frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto transmitStep = dynamic_cast(context->getLastStep()); + if (transmitStep == nullptr) + transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (transmitStep == nullptr) + return false; + if (transmitStep->getFrameToTransmit() == packet) + return isManagementTransactionCancelled(packet) || (dynamic_cast(transmitStep) != nullptr && + isManagementTransactionCancelled(dynamic_cast(transmitStep)->getProtectedFrame())); + auto rtsTransmitStep = dynamic_cast(transmitStep); + return rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet && isManagementTransactionCancelled(packet); +} + +bool Dcf::cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelManagementTransaction"); + auto eventNumber = cSimulation::getActiveSimulation()->getEventNumber(); + if (completedManagementTransactionsEventNumber != eventNumber) { + completedManagementTransactions.clear(); + completedManagementTransactionsEventNumber = eventNumber; + } + if (completedManagementTransactions.find(transactionId) != completedManagementTransactions.end()) + return false; + if (!managementTransactionsBeingCancelled.insert(transactionId).second) + return false; + + // IEEE Std 802.11-2024, 10.3.4.4 and 10.4: terminal retry/lifetime + // failure discards the MMPDU and all remaining fragments. The callback's + // packet is still borrowed by the active frame sequence, so it is left + // for the caller to retire after this helper returns. + + auto belongsToTransaction = [transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + + auto pendingQueue = channelAccess->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + + auto inProgressFrames = channelAccess->getInProgressFrames(); + bool frameSequenceCancellationRequested = false; + bool pendingTransmissionCancelled = false; + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + if (isPacketReferencedByCurrentFrameSequence(packet)) { + // Keep the packet alive for raw pointers held by the active + // sequence, but remove it from eligibility immediately. The + // sequence retires it at its next safe boundary. + inProgressFrames->dropFrame(packet); + cancelledManagementTransactions.insert(transactionId); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + bool currentFrameSequenceCancelled = isCurrentFrameSequenceCancelled(packet); + frameSequenceCancellationRequested |= currentFrameSequenceCancelled; + // A sequence can retain completed steps in its context. Only + // cancel Tx when the current transmit/protected step belongs + // to this transaction; the callback owner alone is not enough + // to identify a historical frame. + if (currentFrameSequenceCancelled && tx != nullptr) + pendingTransmissionCancelled |= tx->cancelPendingTransmission(this); + continue; + } + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(packet, header); + ackHandler->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + if (frameSequenceCancellationRequested && frameSequenceHandler != nullptr) { + frameSequenceHandler->cancelFrameSequence(); + if (pendingTransmissionCancelled) + frameSequenceHandler->abortFrameSequence(); + } + managementTransactionsBeingCancelled.erase(transactionId); + completedManagementTransactions.insert(transactionId); + return true; +} + void Dcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -226,6 +365,7 @@ void Dcf::frameSequenceFinished() if (hasFrameToTransmit()) channelAccess->requestChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review + cancelledManagementTransactions.clear(); } bool Dcf::isReceptionInProgress() @@ -288,12 +428,24 @@ bool Dcf::hasFrameToTransmit() void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) { Enter_Method("originatorProcessRtsProtectionFailed"); + if (isManagementTransactionCancelled(packet)) { + auto protectedHeader = packet->peekAtFront(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardRtsFrame(protectedHeader); + channelAccess->getInProgressFrames()->dropFrame(packet); + if (ackHandler != nullptr) + ackHandler->dropFrame(protectedHeader); + return; + } EV_INFO << "RTS frame transmission failed\n"; auto protectedHeader = packet->peekAtFront(); recoveryProcedure->rtsFrameTransmissionFailed(protectedHeader, stationRetryCounters); EV_INFO << "For the current frame exchange, we have CW = " << channelAccess->getCw() << " SRC = " << recoveryProcedure->getShortRetryCount(packet, protectedHeader) << " LRC = " << recoveryProcedure->getLongRetryCount(packet, protectedHeader) << " SSRC = " << stationRetryCounters->getStationShortRetryCount() << " and SLRC = " << stationRetryCounters->getStationLongRetryCount() << std::endl; if (recoveryProcedure->isRtsFrameRetryLimitReached(packet, protectedHeader)) { recoveryProcedure->retryLimitReached(packet, protectedHeader); + auto transactionTag = packet->findTag(); + bool notifyManagement = dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); channelAccess->getInProgressFrames()->dropFrame(packet); ackHandler->dropFrame(protectedHeader); EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << ", because retry limit is reached.\n"; @@ -302,7 +454,7 @@ void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) details.setLimit(recoveryProcedure->getShortRetryLimit()); emit(packetDroppedSignal, packet, &details); emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -313,8 +465,12 @@ void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) void Dcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); + if (isCurrentFrameSequenceCancelled(packet)) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; emit(packetSentToPeerSignal, packet); + if (isCurrentFrameSequenceCancelled(packet)) + return; auto transmittedHeader = packet->peekAtFront(); if (auto dataOrMgmtHeader = dynamicPtrCast(transmittedHeader)) { EV_INFO << "For the current frame exchange, we have CW = " << channelAccess->getCw() << " SRC = " << recoveryProcedure->getShortRetryCount(packet, dataOrMgmtHeader) << " LRC = " << recoveryProcedure->getLongRetryCount(packet, dataOrMgmtHeader) << " SSRC = " << stationRetryCounters->getStationShortRetryCount() << " and SLRC = " << stationRetryCounters->getStationLongRetryCount() << std::endl; @@ -368,6 +524,15 @@ void Dcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTra void Dcf::originatorProcessFailedFrame(Packet *failedPacket) { Enter_Method("originatorProcessFailedFrame"); + if (isManagementTransactionCancelled(failedPacket)) { + auto failedHeader = failedPacket->peekAtFront(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(failedPacket, failedHeader); + channelAccess->getInProgressFrames()->dropFrame(failedPacket); + if (ackHandler != nullptr) + ackHandler->dropFrame(failedHeader); + return; + } EV_INFO << "Data/Mgmt frame transmission failed\n"; const auto& failedHeader = failedPacket->peekAtFront(); ASSERT(failedHeader->getType() != ST_DATA_WITH_QOS); @@ -381,6 +546,9 @@ void Dcf::originatorProcessFailedFrame(Packet *failedPacket) ackHandler->processFailedFrame(failedHeader); if (retryLimitReached) { recoveryProcedure->retryLimitReached(failedPacket, failedHeader); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); channelAccess->getInProgressFrames()->dropFrame(failedPacket); ackHandler->dropFrame(failedHeader); EV_INFO << "Dropping frame " << failedPacket->getName() << ", because retry limit is reached.\n"; @@ -389,7 +557,7 @@ void Dcf::originatorProcessFailedFrame(Packet *failedPacket) details.setLimit(-1); // TODO emit(packetDroppedSignal, failedPacket, &details); emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 19995cb221a..b77da154fc3 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -8,6 +8,8 @@ #ifndef __INET_DCF_H #define __INET_DCF_H +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" @@ -80,6 +82,17 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; + // A management transaction may have several fragmented MPDUs in the + // pending/in-progress queues. Keep the transaction identity only while + // removing its siblings so queue notifications cannot report the same logical + // transaction recursively. A transaction has one pending original before + // fragmentation; the completed set additionally spans the synchronous + // notifications of a bulk queue removal and is cleared at the next event. + std::set managementTransactionsBeingCancelled; + std::set completedManagementTransactions; + eventnumber_t completedManagementTransactionsEventNumber = -1; + std::set cancelledManagementTransactions; + // Station counters StationRetryCounters *stationRetryCounters = nullptr; @@ -98,6 +111,11 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); + virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual bool isManagementTransactionCancelled(const Packet *packet) const; + virtual bool isCurrentFrameSequenceCancelled(const Packet *packet) const; + virtual bool cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket); + protected: // IChannelAccess::ICallback virtual void channelGranted(IChannelAccess *channelAccess) override; @@ -126,6 +144,8 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Dcf(); + virtual void cancelManagementTransaction(uint64_t transactionId); + // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; virtual void processLowerFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 52b2cf7699f..fe16bb35319 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -173,7 +173,8 @@ void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemo // HCF treats explicit REMOVED notifications as terminal transaction // disposal; code relocating a packet must use dequeuePacket(). if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) { - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr && cancelManagementTransaction(transactionTag->getTransactionId(), packet)) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -185,6 +186,99 @@ void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemo resumeEligibleChannelAccess(); } +void Hcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); +} + +bool Hcf::cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelManagementTransaction"); + auto eventNumber = cSimulation::getActiveSimulation()->getEventNumber(); + if (completedManagementTransactionsEventNumber != eventNumber) { + completedManagementTransactions.clear(); + completedManagementTransactionsEventNumber = eventNumber; + } + if (completedManagementTransactions.find(transactionId) != completedManagementTransactions.end()) + return false; + if (!managementTransactionsBeingCancelled.insert(transactionId).second) + return false; + + // IEEE Std 802.11-2024, 10.3.4.4 and 10.4: terminal retry/lifetime + // failure discards the MMPDU and all remaining fragments. The callback's + // packet is still borrowed by the active frame sequence, so it is left + // for the caller to retire after this helper returns. + + auto belongsToTransaction = [transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + bool frameSequenceCancellationRequested = false; + bool pendingTransmissionCancelled = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + if (isPacketReferencedByCurrentFrameSequence(packet)) { + // Keep the packet alive for raw pointers held by the + // active sequence, but remove it from eligibility + // immediately. The sequence retires it at its next safe + // boundary. + inProgressFrames->dropFrame(packet); + cancelledManagementTransactions.insert(transactionId); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + bool currentFrameSequenceCancelled = isCurrentFrameSequenceCancelled(packet); + frameSequenceCancellationRequested |= currentFrameSequenceCancelled; + // A sequence can retain completed steps in its context. + // Only cancel Tx when the current transmit/protected step + // belongs to this transaction; the callback owner alone + // is not enough to identify a historical frame. + if (currentFrameSequenceCancelled && tx != nullptr) + pendingTransmissionCancelled |= tx->cancelPendingTransmission(this); + continue; + } + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + auto managementRecoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (managementRecoveryProcedure != nullptr) + managementRecoveryProcedure->discardFrame(packet, header); + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + } + if (frameSequenceCancellationRequested && frameSequenceHandler != nullptr) { + frameSequenceHandler->cancelFrameSequence(); + if (pendingTransmissionCancelled) + frameSequenceHandler->abortFrameSequence(); + } + managementTransactionsBeingCancelled.erase(transactionId); + completedManagementTransactions.insert(transactionId); + return true; +} + std::string Hcf::getFrameSequenceInfo() const { if (!frameSequenceHandler->isSequenceRunning()) @@ -294,6 +388,33 @@ bool Hcf::isPacketReferencedByCurrentFrameSequence(const Packet *packet) const return false; } +bool Hcf::isManagementTransactionCancelled(const Packet *packet) const +{ + if (packet == nullptr) + return false; + auto transactionTag = packet->findTag(); + return transactionTag != nullptr && cancelledManagementTransactions.find(transactionTag->getTransactionId()) != cancelledManagementTransactions.end(); +} + +bool Hcf::isCurrentFrameSequenceCancelled(const Packet *packet) const +{ + if (packet == nullptr || frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto transmitStep = dynamic_cast(context->getLastStep()); + if (transmitStep == nullptr) + transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (transmitStep == nullptr) + return false; + if (transmitStep->getFrameToTransmit() == packet) + return isManagementTransactionCancelled(packet) || (dynamic_cast(transmitStep) != nullptr && + isManagementTransactionCancelled(dynamic_cast(transmitStep)->getProtectedFrame())); + auto rtsTransmitStep = dynamic_cast(transmitStep); + return rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet && isManagementTransactionCancelled(packet); +} + void Hcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -469,6 +590,9 @@ int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else if (auto mgmtHeader = dynamicPtrCast(internallyCollidedHeader)) edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(internallyCollidedFrame, mgmtHeader); else ; // TODO + NonQoSDataFrame + auto transactionTag = internallyCollidedFrame->findTag(); + bool notifyManagement = dynamicPtrCast(internallyCollidedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), internallyCollidedFrame)); edcaf->getInProgressFrames()->dropFrame(internallyCollidedFrame); processDroppedBlockAckSetupFrame(internallyCollidedFrame); processDroppedBlockAckTeardownFrame(internallyCollidedFrame); @@ -478,7 +602,7 @@ int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) details.setLimit(-1); // TODO emit(packetDroppedSignal, internallyCollidedFrame, &details); emit(linkBrokenSignal, internallyCollidedFrame); - if (dynamicPtrCast(internallyCollidedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, internallyCollidedFrame, &transmissionDetails); @@ -517,6 +641,7 @@ void Hcf::frameSequenceFinished() } else throw cRuntimeError("Frame sequence finished but channel owner not found!"); + cancelledManagementTransactions.clear(); } void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header) @@ -642,6 +767,19 @@ void Hcf::transmissionComplete(Packet *packet, const PtrpeekAtFront(); + auto edcaf = edca->getChannelOwner(); + if (edcaf != nullptr) { + auto recoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardRtsFrame(protectedHeader); + edcaf->getInProgressFrames()->dropFrame(packet); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(protectedHeader); + } + return; + } auto protectedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { @@ -669,6 +807,9 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else edca->getMgmtAndNonQoSRecoveryProcedure()->discardRtsFrame(addbaRequest); + auto transactionTag = packet->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); edcaf->getInProgressFrames()->dropFrame(packet); processDroppedBlockAckSetupFrame(packet); processDroppedBlockAckTeardownFrame(packet); @@ -681,7 +822,7 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) emit(packetDroppedSignal, packet, &details); if (retryLimitReached) { emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -696,11 +837,15 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) void Hcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); + if (isCurrentFrameSequenceCancelled(packet)) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; auto transmittedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { edcaf->emit(packetSentToPeerSignal, packet); + if (isCurrentFrameSequenceCancelled(packet)) + return; AccessCategory ac = edcaf->getAccessCategory(); if (transmittedHeader->getReceiverAddress().isMulticast()) { edcaf->getRecoveryProcedure()->multicastFrameTransmitted(); @@ -776,6 +921,19 @@ void Hcf::originatorProcessTransmittedControlFrame(const PtrpeekAtFront(); + auto edcaf = edca->getChannelOwner(); + if (edcaf != nullptr) { + auto recoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(failedPacket, failedHeader); + edcaf->getInProgressFrames()->dropFrame(failedPacket); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(failedHeader); + } + return; + } auto failedHeader = failedPacket->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { @@ -818,6 +976,9 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else edca->getMgmtAndNonQoSRecoveryProcedure()->discardFrame(failedPacket, addbaRequest); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); edcaf->getInProgressFrames()->dropFrame(failedPacket); processDroppedBlockAckSetupFrame(failedPacket); processDroppedBlockAckTeardownFrame(failedPacket); @@ -830,7 +991,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) emit(packetDroppedSignal, failedPacket, &details); if (retryLimitReached) { emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index a24eb16ba42..53a53e2361b 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -10,6 +10,7 @@ #include #include +#include #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" @@ -111,6 +112,17 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; + // A management transaction may have several fragmented MPDUs in the + // pending/in-progress queues. Keep the transaction identity only while + // removing its siblings so queue notifications cannot report the same logical + // transaction recursively. A transaction has one pending original before + // fragmentation; the completed set additionally spans the synchronous + // notifications of a bulk queue removal and is cleared at the next event. + std::set managementTransactionsBeingCancelled; + std::set completedManagementTransactions; + eventnumber_t completedManagementTransactionsEventNumber = -1; + std::set cancelledManagementTransactions; + // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -133,6 +145,9 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual bool processDroppedBlockAckSetupFrame(Packet *packet); virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual bool isManagementTransactionCancelled(const Packet *packet) const; + virtual bool isCurrentFrameSequenceCancelled(const Packet *packet) const; + virtual bool cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket); virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); @@ -188,6 +203,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Hcf(); + virtual void cancelManagementTransaction(uint64_t transactionId); + // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; virtual void processLowerFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index 1eddb2b2048..a70939b288b 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -31,6 +31,11 @@ void FrameSequenceHandler::handleStartRxTimeout() void FrameSequenceHandler::processResponse(Packet *frame) { ASSERT(callback != nullptr); + if (frameSequenceCancellationRequested) { + delete frame; + abortFrameSequence(); + return; + } auto lastStep = context->getLastStep(); switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { @@ -52,7 +57,10 @@ void FrameSequenceHandler::processResponse(Packet *frame) void FrameSequenceHandler::transmissionComplete() { if (isSequenceRunning()) { - finishFrameSequenceStep(); + if (frameSequenceCancellationRequested) + abortFrameSequence(); + else + finishFrameSequenceStep(); if (isSequenceRunning()) startFrameSequenceStep(); } @@ -63,6 +71,7 @@ void FrameSequenceHandler::startFrameSequence(IFrameSequence *frameSequence, Fra EV_INFO << "Starting frame sequence.\n"; this->callback = callback; if (!isSequenceRunning()) { + frameSequenceCancellationRequested = false; this->frameSequence = frameSequence; this->context = context; frameSequence->startSequence(context, 0); @@ -75,6 +84,10 @@ void FrameSequenceHandler::startFrameSequence(IFrameSequence *frameSequence, Fra void FrameSequenceHandler::startFrameSequenceStep() { ASSERT(isSequenceRunning()); + if (frameSequenceCancellationRequested) { + abortFrameSequence(); + return; + } auto nextStep = frameSequence->prepareStep(context); EV_INFO << "Starting next frame sequence step: history = " << frameSequence->getHistory() << "\n"; if (nextStep == nullptr) @@ -119,6 +132,8 @@ void FrameSequenceHandler::finishFrameSequenceStep() case IFrameSequenceStep::Type::TRANSMIT: { auto transmitStep = static_cast(lastStep); callback->originatorProcessTransmittedFrame(transmitStep->getFrameToTransmit()); + if (frameSequenceCancellationRequested && isSequenceRunning()) + abortFrameSequence(); break; } case IFrameSequenceStep::Type::RECEIVE: { @@ -143,6 +158,7 @@ void FrameSequenceHandler::finishFrameSequence() context = nullptr; frameSequence = nullptr; callback = nullptr; + frameSequenceCancellationRequested = false; inProgressFrames->clearDroppedFrames(); } @@ -166,6 +182,7 @@ void FrameSequenceHandler::abortFrameSequence() context = nullptr; frameSequence = nullptr; callback = nullptr; + frameSequenceCancellationRequested = false; inProgressFrames->clearDroppedFrames(); } diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index 370295cc6f7..4dca5f0c99d 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -21,12 +21,12 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler IFrameSequenceHandler::ICallback *callback = nullptr; IFrameSequence *frameSequence = nullptr; FrameSequenceContext *context = nullptr; + bool frameSequenceCancellationRequested = false; protected: virtual void startFrameSequenceStep(); virtual void finishFrameSequenceStep(); virtual void finishFrameSequence(); - virtual void abortFrameSequence(); public: virtual const FrameSequenceContext *getContext() const override { return context; } @@ -36,6 +36,8 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } + virtual void cancelFrameSequence() override { if (isSequenceRunning()) frameSequenceCancellationRequested = true; } + virtual void abortFrameSequence() override; virtual ~FrameSequenceHandler(); }; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index c3e3106e75b..5b87f6795eb 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -63,6 +63,7 @@ void Ieee80211MgmtAp::initialize(int stage) beaconTimer = new cMessage("beaconTimer"); auto macModule = getModuleFromPar(par("macModule"), this); macModule->subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + managementFrameTransactionHandler.reference(this, "macModule", true); } } @@ -204,6 +205,17 @@ void Ieee80211MgmtAp::clearPendingAssociation(StaInfo *sta) sta->pendingHtOperation = Ieee80211HtOperation(); } +void Ieee80211MgmtAp::supersedePendingAssociation(StaInfo *sta) +{ + auto transactionId = sta->pendingAssociationTransactionId; + // Clear the AP bookkeeping before entering the MAC so synchronous queue + // callbacks cannot mistake this superseded frame for the active + // transaction. + clearPendingAssociation(sta); + if (transactionId != 0 && managementFrameTransactionHandler) + managementFrameTransactionHandler->cancelManagementTransaction(transactionId); +} + void Ieee80211MgmtAp::sendBeacon() { EV << "Sending beacon\n"; @@ -246,7 +258,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrgetPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); @@ -309,7 +321,7 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrgetPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; // mark STA as not authenticated; alternatively, it could also be removed from staList if (wasAssociated) @@ -490,7 +502,7 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrgetPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); @@ -563,6 +575,8 @@ void Ieee80211MgmtAp::start() void Ieee80211MgmtAp::stop() { cancelEvent(beaconTimer); + for (auto& entry : staList) + supersedePendingAssociation(&entry.second); staList.clear(); nextAssociationTransactionId = 0; mib->clearAssociationIds(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index fe8c0c03192..afe48285491 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -11,6 +11,7 @@ #include #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h" namespace inet { @@ -73,6 +74,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase StaList staList; ///< list of STAs cMessage *beaconTimer = nullptr; uint64_t nextAssociationTransactionId = 0; + ModuleRefByPar managementFrameTransactionHandler; public: Ieee80211MgmtAp() {} @@ -101,6 +103,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase virtual uint64_t createAssociationTransactionId(); virtual void clearPendingAssociation(StaInfo *sta); + virtual void supersedePendingAssociation(StaInfo *sta); /** Classifies a terminal management-MPDU result using its transaction tag and fragment state. */ static AssociationResponseDisposition getAssociationResponseDisposition(const Packet *responseFrame, diff --git a/tests/module/Ieee80211MgmtApCancellation_1.test b/tests/module/Ieee80211MgmtApCancellation_1.test new file mode 100644 index 00000000000..19da98e10df --- /dev/null +++ b/tests/module/Ieee80211MgmtApCancellation_1.test @@ -0,0 +1,656 @@ +%description: +Verify that superseding an AP association response cancels every tagged +fragment owned by the real DCF or HCF MAC. A deterministic contention module +holds the response until the test grants access, and a test MAC holds the +first frame handoff so authentication restart, deauthentication, and +disassociation can all cancel an active response before it reaches the PHY. +The test also cancels a DCF response after CTS and a HCF response during its +IFS, before either response crosses the Tx-to-MAC handoff. Replacement +responses are then completed through the ordinary ACK path. + +%file: TestIeee80211MgmtApCancellation.cc + +#include +#include +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Tx.h" +#include "inet/linklayer/ieee80211/mac/contention/Contention.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace ieee80211 { + +class TestBlockedContention : public Contention +{ + protected: + IContention::ICallback *blockedCallback = nullptr; + + public: + virtual void startContention(int, simtime_t, simtime_t, simtime_t, IContention::ICallback *callback) override + { + Enter_Method("startContention"); + ASSERT(blockedCallback == nullptr); + blockedCallback = callback; + } + + virtual bool isContentionInProgress() override { return blockedCallback != nullptr; } + + bool hasBlockedContention() const { return blockedCallback != nullptr; } + + void grantAccess() + { + Enter_Method("grantAccess"); + ASSERT(blockedCallback != nullptr); + auto callback = blockedCallback; + blockedCallback = nullptr; + callback->channelAccessGranted(); + } +}; + +Define_Module(TestBlockedContention); + +class TestInspectableTx : public Tx +{ + protected: + bool holdDelayedAssociationResponse = false; + + public: + bool isWaitingForIfs() const { return frame != nullptr && !transmitting && endIfsTimer != nullptr && endIfsTimer->isScheduled(); } + + void setHoldDelayedAssociationResponse(bool hold) { holdDelayedAssociationResponse = hold; } + + virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ITx::ICallback *callback) override + { + Enter_Method("transmitFrame"); + bool hold = holdDelayedAssociationResponse && ifs > 0 && (header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE); + Tx::transmitFrame(packet, header, ifs, callback); + if (hold && !transmitting && endIfsTimer->isScheduled()) { + cancelEvent(endIfsTimer); + scheduleAfter(SimTime(1, SIMTIME_S), endIfsTimer); + } + } +}; + +Define_Module(TestInspectableTx); + +class TestHoldingIeee80211Mac : public Ieee80211Mac +{ + protected: + Packet *heldFrame = nullptr; + bool holdNextFrame = true; + int rtsHandoffCount = 0; + int ctsReceivedCount = 0; + std::vector managementHandoffTransactionIds; + + void discardHeldFrame() + { + if (heldFrame != nullptr) { + auto frame = heldFrame; + heldFrame = nullptr; + if (frame->getOwner() == this) + drop(frame); + delete frame; + } + } + + public: + virtual ~TestHoldingIeee80211Mac() { discardHeldFrame(); } + + virtual void sendDownFrame(Packet *frame) override + { + auto header = frame->peekAtFront(); + if (header->getType() == ST_RTS) + rtsHandoffCount++; + if (header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE) { + auto transactionTag = frame->findTag(); + if (transactionTag != nullptr) + managementHandoffTransactionIds.push_back(transactionTag->getTransactionId()); + } + if (holdNextFrame) { + ASSERT(heldFrame == nullptr); + holdNextFrame = false; + take(frame); + heldFrame = frame; + } + else + Ieee80211Mac::sendDownFrame(frame); + } + + virtual void processLowerFrame(Packet *packet, const Ptr& header) override + { + if (header->getType() == ST_CTS) + ctsReceivedCount++; + Ieee80211Mac::processLowerFrame(packet, header); + } + + bool hasHeldFrame() const { return heldFrame != nullptr; } + + bool isWaitingForIfs() const + { + auto tx = check_and_cast(getSubmodule("tx")); + return tx->isWaitingForIfs(); + } + + int getRtsHandoffCount() const { return rtsHandoffCount; } + int getCtsReceivedCount() const { return ctsReceivedCount; } + int getManagementHandoffCount(uint64_t transactionId) const { return std::count(managementHandoffTransactionIds.begin(), managementHandoffTransactionIds.end(), transactionId); } + + void disarmHold() + { + Enter_Method("disarmHold"); + ASSERT(heldFrame == nullptr); + holdNextFrame = false; + } + + void armDelayedAssociationResponseHold() + { + Enter_Method("armDelayedAssociationResponseHold"); + auto tx = check_and_cast(getSubmodule("tx")); + tx->setHoldDelayedAssociationResponse(true); + } + + void disarmDelayedAssociationResponseHold() + { + Enter_Method("disarmDelayedAssociationResponseHold"); + auto tx = check_and_cast(getSubmodule("tx")); + tx->setHoldDelayedAssociationResponse(false); + } + + void armHold() + { + Enter_Method("armHold"); + ASSERT(heldFrame == nullptr); + holdNextFrame = true; + } + + void completeHeldTransmission() + { + Enter_Method("completeHeldTransmission"); + ASSERT(heldFrame != nullptr); + auto tx = check_and_cast(getSubmodule("tx")); + tx->radioTransmissionFinished(); + discardHeldFrame(); + } +}; + +Define_Module(TestHoldingIeee80211Mac); + +class TestIeee80211MgmtApCancellation : public Ieee80211MgmtAp +{ + public: + std::vector responseStatuses; + std::vector responseTransactionIds; + + virtual void start() override + { + // Keep beacon traffic out of the deterministic transaction exchange. + Ieee80211MgmtApBase::start(); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, int subtype, + const MacAddress& destAddr, uint64_t transactionId = 0) override + { + // Authentication is driven directly by the test and its response is + // irrelevant to the AP association-response transaction. + if (subtype == ST_AUTHENTICATION) + return; + Ieee80211MgmtAp::sendManagementFrame(name, body, subtype, destAddr, transactionId); + } + + void markAuthenticated(const MacAddress& address) + { + Enter_Method("markAuthenticated"); + auto& sta = staList[address]; + sta.address = address; + sta.authSeqExpected = 1; + mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + } + + void submitAssociationRequest(const MacAddress& address) + { + Enter_Method("submitAssociationRequest"); + auto packet = new Packet("AssociationRequest"); + auto body = makeShared(); + body->setSSID("SSID"); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1; + body->setSupportedRates(rates); + body->setChunkLength(B(1)); + packet->insertAtBack(body); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleAssociationRequestFrame(packet, header); + } + + void submitAuthentication(const MacAddress& address, int sequenceNumber) + { + Enter_Method("submitAuthentication"); + auto packet = new Packet("Authentication"); + auto body = makeShared(); + body->setSequenceNumber(sequenceNumber); + body->setStatusCode(SC_SUCCESSFUL); + packet->insertAtBack(body); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleAuthenticationFrame(packet, header); + } + + void submitDeauthentication(const MacAddress& address) + { + Enter_Method("submitDeauthentication"); + auto packet = new Packet("Deauthentication"); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleDeauthenticationFrame(packet, header); + } + + void submitDisassociation(const MacAddress& address) + { + Enter_Method("submitDisassociation"); + auto packet = new Packet("Disassociation"); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleDisassociationFrame(packet, header); + } + + bool hasPendingAssociation(const MacAddress& address) const + { + auto it = staList.find(address); + return it != staList.end() && it->second.pendingAssociationTransactionId != 0; + } + + uint64_t getPendingAssociationTransactionId(const MacAddress& address) const + { + return staList.at(address).pendingAssociationTransactionId; + } + + 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(); + } + + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const + { + return mib->bssAccessPointData.stations.at(address); + } + + protected: + virtual void frameTransmissionFinished(const Packet *frame, FrameTransmissionStatus status) override + { + responseStatuses.push_back(status); + auto transactionTag = frame == nullptr ? nullptr : frame->findTag(); + responseTransactionIds.push_back(transactionTag == nullptr ? 0 : transactionTag->getTransactionId()); + Ieee80211MgmtAp::frameTransmissionFinished(frame, status); + } +}; + +Define_Module(TestIeee80211MgmtApCancellation); + +class TransactionSignalListener : public cListener +{ + public: + std::vector sentTransactionIds; + std::vector lowerTransactionIds; + std::vector droppedTransactionIds; + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *value, cObject *) override + { + auto packet = dynamic_cast(value); + if (packet == nullptr || (signalID != packetSentToPeerSignal && signalID != packetSentToLowerSignal && signalID != packetDroppedSignal)) + return; + auto header = packet->peekAtFront(); + if (header->getType() != ST_ASSOCIATIONRESPONSE && header->getType() != ST_REASSOCIATIONRESPONSE) + return; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return; + if (signalID == packetSentToPeerSignal) + sentTransactionIds.push_back(transactionTag->getTransactionId()); + else if (signalID == packetSentToLowerSignal) + lowerTransactionIds.push_back(transactionTag->getTransactionId()); + else + droppedTransactionIds.push_back(transactionTag->getTransactionId()); + } +}; + +class Ieee80211MgmtApCancellationTest : public cSimpleModule +{ + protected: + enum class Supersession { + AUTHENTICATION_RESTART, + DEAUTHENTICATION, + DISASSOCIATION, + }; + + static int countTransaction(queueing::IPacketQueue *queue, uint64_t transactionId) + { + int count = 0; + for (int i = 0; i < queue->getNumPackets(); i++) { + auto transactionTag = queue->getPacket(i)->findTag(); + if (transactionTag != nullptr && transactionTag->getTransactionId() == transactionId) + count++; + } + return count; + } + + static int countTransaction(InProgressFrames *frames, uint64_t transactionId) + { + int count = 0; + for (int i = 0; i < frames->getLength(); i++) { + auto transactionTag = frames->getFrames(i)->findTag(); + if (transactionTag != nullptr && transactionTag->getTransactionId() == transactionId) + count++; + } + return count; + } + + void runActiveCancellation(const char *apName, bool qos, const MacAddress& station, Supersession supersession, bool expectReplacement, bool cancelDuringIfs = false) + { + auto apBase = std::string("^.") + apName + ".wlan[0]"; + auto mgmt = check_and_cast(getModuleByPath((apBase + ".mgmt").c_str())); + auto mac = check_and_cast(getModuleByPath((apBase + ".mac").c_str())); + auto pendingQueuePath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].pendingQueue" : ".mac.dcf.channelAccess.pendingQueue"); + auto inProgressFramesPath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].inProgressFrames" : ".mac.dcf.channelAccess.inProgressFrames"); + auto contentionPath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].contention" : ".mac.dcf.channelAccess.contention"); + auto dropSignalSourcePath = apBase + (qos ? ".mac.hcf" : ".mac.dcf"); + auto sentSignalSourcePath = apBase + (qos ? ".mac.hcf.edca.edcaf[3]" : ".mac.dcf"); + auto pendingQueue = check_and_cast(getModuleByPath(pendingQueuePath.c_str())); + auto inProgressFrames = check_and_cast(getModuleByPath(inProgressFramesPath.c_str())); + auto contention = check_and_cast(getModuleByPath(contentionPath.c_str())); + auto dropSignalSource = getModuleByPath(dropSignalSourcePath.c_str()); + auto sentSignalSource = getModuleByPath(sentSignalSourcePath.c_str()); + TransactionSignalListener signalListener; + sentSignalSource->subscribe(packetSentToPeerSignal, &signalListener); + mac->subscribe(packetSentToLowerSignal, &signalListener); + dropSignalSource->subscribe(packetDroppedSignal, &signalListener); + + if (supersession != Supersession::DISASSOCIATION) + mgmt->markAuthenticated(station); + mgmt->submitAssociationRequest(station); + auto oldTransactionId = mgmt->getPendingAssociationTransactionId(station); + auto oldAssociationId = mgmt->reserveAssociationId(station); + ASSERT(oldTransactionId != 0); + ASSERT(oldAssociationId != 0); + for (int i = 0; i < 1000 && countTransaction(pendingQueue, oldTransactionId) == 0; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(countTransaction(pendingQueue, oldTransactionId) > 0); + ASSERT(inProgressFrames->getLength() == 0); + ASSERT(contention->hasBlockedContention()); + + auto oldRtsHandoffCount = mac->getRtsHandoffCount(); + auto oldCtsReceivedCount = mac->getCtsReceivedCount(); + auto oldManagementHandoffCount = mac->getManagementHandoffCount(oldTransactionId); + if (cancelDuringIfs) { + mac->disarmHold(); + mac->armDelayedAssociationResponseHold(); + } + contention->grantAccess(); + if (cancelDuringIfs) { + for (int i = 0; i < 1000 && (!mac->isWaitingForIfs() || mac->getCtsReceivedCount() == oldCtsReceivedCount || mac->getRtsHandoffCount() == oldRtsHandoffCount); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mac->isWaitingForIfs()); + ASSERT(mac->getCtsReceivedCount() > oldCtsReceivedCount); + ASSERT(mac->getRtsHandoffCount() > oldRtsHandoffCount); + ASSERT(!mac->hasHeldFrame()); + } + else { + for (int i = 0; i < 1000 && !mac->hasHeldFrame(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mac->hasHeldFrame()); + } + auto oldInProgressCount = countTransaction(inProgressFrames, oldTransactionId); + ASSERT(oldInProgressCount > 0); + auto oldDroppedCount = signalListener.droppedTransactionIds.size(); + auto oldSentCount = signalListener.sentTransactionIds.size(); + auto oldLowerCount = signalListener.lowerTransactionIds.size(); + auto oldCallbackCount = mgmt->responseStatuses.size(); + + if (supersession == Supersession::AUTHENTICATION_RESTART) + mgmt->submitAuthentication(station, 1); + else if (supersession == Supersession::DEAUTHENTICATION) + mgmt->submitDeauthentication(station); + else + mgmt->submitDisassociation(station); + ASSERT(!mgmt->hasPendingAssociation(station)); + ASSERT(countTransaction(pendingQueue, oldTransactionId) == 0); + ASSERT(countTransaction(inProgressFrames, oldTransactionId) == 0); + ASSERT(signalListener.droppedTransactionIds.size() - oldDroppedCount == (size_t)oldInProgressCount); + ASSERT(signalListener.sentTransactionIds.size() == oldSentCount); + ASSERT(signalListener.lowerTransactionIds.size() == oldLowerCount); + ASSERT(mgmt->responseStatuses.size() == oldCallbackCount); + ASSERT(mac->getManagementHandoffCount(oldTransactionId) == oldManagementHandoffCount); + if (cancelDuringIfs) + mac->disarmDelayedAssociationResponseHold(); + + auto reusableAddress = MacAddress((std::string("02:00:00:00:10:") + (qos ? "02" : "01")).c_str()); + if (!mgmt->hasCommittedAssociationId(station)) { + ASSERT(mgmt->reserveAssociationId(reusableAddress) == oldAssociationId); + mgmt->cancelAssociationIdReservation(reusableAddress); + } + + uint64_t replacementTransactionId = 0; + if (expectReplacement) { + if (supersession == Supersession::DEAUTHENTICATION) + mgmt->submitAuthentication(station, 1); + ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::AUTHENTICATED); + mgmt->submitAssociationRequest(station); + replacementTransactionId = mgmt->getPendingAssociationTransactionId(station); + ASSERT(replacementTransactionId != 0 && replacementTransactionId != oldTransactionId); + ASSERT(mgmt->hasPendingAssociation(station)); + } + + if (!cancelDuringIfs) { + mac->completeHeldTransmission(); + ASSERT(!mac->hasHeldFrame()); + } + else { + ASSERT(!mac->hasHeldFrame()); + ASSERT(!mac->isWaitingForIfs()); + } + if (expectReplacement) { + for (int i = 0; i < 1000 && countTransaction(pendingQueue, replacementTransactionId) == 0; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(countTransaction(pendingQueue, replacementTransactionId) > 0); + ASSERT(contention->hasBlockedContention()); + // The DCF delayed case has already sent its RTS and the station's + // NAV covers the cancelled exchange. Let that reservation expire + // before starting the replacement; no replacement frame is + // eligible to cross the handoff during this interval. + if (cancelDuringIfs && !qos) + wait(SimTime(1, SIMTIME_MS)); + contention->grantAccess(); + for (int i = 0; i < 100000 && mgmt->hasPendingAssociation(station); i++) { + if (contention->hasBlockedContention()) + contention->grantAccess(); + wait(SimTime(1, SIMTIME_US)); + } + ASSERT(mgmt->responseStatuses.size() > oldCallbackCount); + ASSERT(!mgmt->hasPendingAssociation(station)); + ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); + ASSERT(mgmt->hasCommittedAssociationId(station)); + ASSERT(mgmt->reserveAssociationId(station) == oldAssociationId); + for (size_t i = oldCallbackCount; i < mgmt->responseTransactionIds.size(); i++) + ASSERT(mgmt->responseTransactionIds[i] == replacementTransactionId); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), oldTransactionId) == signalListener.sentTransactionIds.end()); + ASSERT(std::find(signalListener.lowerTransactionIds.begin(), signalListener.lowerTransactionIds.end(), oldTransactionId) == signalListener.lowerTransactionIds.end()); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), replacementTransactionId) != signalListener.sentTransactionIds.end()); + } + else { + for (int i = 0; i < 100; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->responseStatuses.size() == oldCallbackCount); + ASSERT(!mgmt->hasCommittedAssociationId(station)); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), oldTransactionId) == signalListener.sentTransactionIds.end()); + ASSERT(std::find(signalListener.lowerTransactionIds.begin(), signalListener.lowerTransactionIds.end(), oldTransactionId) == signalListener.lowerTransactionIds.end()); + } + + sentSignalSource->unsubscribe(packetSentToPeerSignal, &signalListener); + mac->unsubscribe(packetSentToLowerSignal, &signalListener); + dropSignalSource->unsubscribe(packetDroppedSignal, &signalListener); + } + + public: + Ieee80211MgmtApCancellationTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + runActiveCancellation("apDcf", false, MacAddress("02:00:00:00:00:09"), Supersession::AUTHENTICATION_RESTART, true); + runActiveCancellation("apDcf", false, MacAddress("02:00:00:00:00:09"), Supersession::AUTHENTICATION_RESTART, true, true); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::DEAUTHENTICATION, true); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::AUTHENTICATION_RESTART, true, true); + auto hcfMac = check_and_cast(getModuleByPath("^.apHcf.wlan[0].mac")); + hcfMac->armHold(); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::DISASSOCIATION, false); + std::cout << "AP DCF/HCF association transaction supersession cancellation and replacement completion verified.\n"; + } +}; + +Define_Module(Ieee80211MgmtApCancellationTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.linklayer.ieee80211.mac.Tx; +import inet.linklayer.ieee80211.mac.contention.Contention; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple TestBlockedContention extends Contention +{ + parameters: + @class(::inet::ieee80211::TestBlockedContention); +} + +simple TestInspectableTx extends Tx +{ + parameters: + @class(::inet::ieee80211::TestInspectableTx); +} + +module TestHoldingIeee80211Mac extends Ieee80211Mac +{ + parameters: + @class(::inet::ieee80211::TestHoldingIeee80211Mac); +} + +simple TestIeee80211MgmtApCancellation extends Ieee80211MgmtAp +{ + parameters: + @class(::inet::ieee80211::TestIeee80211MgmtApCancellation); +} + +simple Ieee80211MgmtApCancellationTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtApCancellationTest); +} + +network Ieee80211MgmtApCancellationTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + apDcf: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtApCancellation"; + wlan[*].mac.typename = "TestHoldingIeee80211Mac"; + wlan[*].mac.tx.typename = "TestInspectableTx"; + wlan[*].mac.dcf.channelAccess.contention.typename = "TestBlockedContention"; + } + staDcf: WirelessHost { + parameters: + wlan[*].mac.typename = "Ieee80211TesterMac"; + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + apHcf: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtApCancellation"; + wlan[*].mac.typename = "TestHoldingIeee80211Mac"; + wlan[*].mac.tx.typename = "TestInspectableTx"; + wlan[*].mac.qosStation = true; + wlan[*].mac.hcf.edca.edcaf[*].contention.typename = "TestBlockedContention"; + } + staHcf: WirelessHost { + parameters: + wlan[*].mac.typename = "Ieee80211TesterMac"; + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + test: Ieee80211MgmtApCancellationTest; + connections allowunconnected: + // The wireless interfaces connect to the shared radio medium through + // their radioIn gates; no wired traffic is needed by this regression. +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtApCancellationTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 100ms +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.apDcf.wlan[0].mgmt.beaconInterval = 10s +*.apHcf.wlan[0].mgmt.beaconInterval = 10s +*.apDcf.wlan[0].mgmt.numAuthSteps = 2 +*.apHcf.wlan[0].mgmt.numAuthSteps = 2 +*.apDcf.wlan[0].address = "02:00:00:00:00:01" +*.apHcf.wlan[0].address = "02:00:00:00:00:02" +*.staDcf.wlan[0].address = "02:00:00:00:00:09" +*.staHcf.wlan[0].address = "02:00:00:00:00:0a" +*.staDcf.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" +*.staHcf.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:02" +*.staDcf.wlan[0].mac.actions = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +*.staHcf.wlan[0].mac.actions = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +*.apDcf.wlan[0].mac.dcf.rtsPolicy.rtsThreshold = 0B +*.apHcf.wlan[0].mac.hcf.rtsPolicy.rtsThreshold = 0B +*.apDcf.wlan[0].mac.dcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 32B +*.apHcf.wlan[0].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 32B +*.apDcf.mobility.initialX = 0m +*.apDcf.mobility.initialY = 0m +*.staDcf.mobility.initialX = 1m +*.staDcf.mobility.initialY = 0m +*.apHcf.mobility.initialX = 0m +*.apHcf.mobility.initialY = 20m +*.staHcf.mobility.initialX = 1m +*.staHcf.mobility.initialY = 20m +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = 11Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.centerFrequency = 2.4GHz +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB + +%contains: stdout +AP DCF/HCF association transaction supersession cancellation and replacement completion verified. diff --git a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test index 67c2368ec26..11e64ff1253 100644 --- a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test @@ -190,6 +190,64 @@ class Ieee80211MgmtApHcfQueueDropTest : public cSimpleModule ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); ASSERT(mgmt->getCommittedAssociationId(station) == committedAid); + // An explicit single-packet removal follows the same terminal path as + // a queue drop: the AP must release both the pending transaction and + // its reserved AID before another station can reserve it. + const MacAddress explicitlyRemovedStation("02:00:00:00:00:0b"); + mgmt->markAuthenticated(explicitlyRemovedStation); + mgmt->submitAssociationRequest(explicitlyRemovedStation); + for (int i = 0; i < 100 && managementQueue->isEmpty(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(explicitlyRemovedStation)); + auto explicitlyRemovedPacket = managementQueue->findPacket([](const Packet *packet) { + return packet->findTag() != nullptr; + }); + ASSERT(explicitlyRemovedPacket != nullptr); + managementQueue->removePacket(explicitlyRemovedPacket); + take(explicitlyRemovedPacket); + delete explicitlyRemovedPacket; + for (int i = 0; i < 100 && mgmt->statuses.size() < 5; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 5); + ASSERT(mgmt->statuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(explicitlyRemovedStation)); + ASSERT(mgmt->getCommittedAssociationId(explicitlyRemovedStation) == 0); + auto explicitlyRemovedAid = mgmt->reserveAssociationId(explicitlyRemovedStation); + ASSERT(explicitlyRemovedAid != 0); + mgmt->cancelAssociationIdReservation(explicitlyRemovedStation); + + // Compound removeAllPackets exercises explicit bulk departure for two + // independent association transactions and must clear both states. + const MacAddress bulkRemovedStation1("02:00:00:00:00:0c"); + const MacAddress bulkRemovedStation2("02:00:00:00:00:0d"); + mgmt->markAuthenticated(bulkRemovedStation1); + mgmt->markAuthenticated(bulkRemovedStation2); + mgmt->submitAssociationRequest(bulkRemovedStation1); + mgmt->submitAssociationRequest(bulkRemovedStation2); + for (int i = 0; i < 100 && managementQueue->getNumPackets() < 2; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation2)); + // The compound boundary is the queue observed by HCF. Its + // removeAllPackets() implementation removes each child packet while + // suppressing the child's duplicate REMOVED notification, then + // reports the terminal departure once at the boundary. + compoundQueue->removeAllPackets(); + for (int i = 0; i < 100 && mgmt->statuses.size() < 7; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 7); + ASSERT(mgmt->statuses[5] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(mgmt->statuses[6] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation2)); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation1) == 0); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation2) == 0); + auto bulkAid1 = mgmt->reserveAssociationId(bulkRemovedStation1); + auto bulkAid2 = mgmt->reserveAssociationId(bulkRemovedStation2); + ASSERT(bulkAid1 != 0 && bulkAid2 != 0 && bulkAid1 != bulkAid2); + mgmt->cancelAssociationIdReservation(bulkRemovedStation1); + mgmt->cancelAssociationIdReservation(bulkRemovedStation2); + std::cout << "HCF compound pending-queue drop cleanup, AID preservation, and reassociation recovery verified.\n"; } }; diff --git a/tests/module/Ieee80211MgmtApQueueDrop_1.test b/tests/module/Ieee80211MgmtApQueueDrop_1.test index a428423bf9c..5479bc0e490 100644 --- a/tests/module/Ieee80211MgmtApQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApQueueDrop_1.test @@ -66,6 +66,11 @@ class TestIeee80211MgmtApQueueDrop : public Ieee80211MgmtAp short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } + short getCommittedAssociationId(const MacAddress& address) const + { + 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->getBssAccessPointData().stations.at(address); } protected: @@ -129,6 +134,59 @@ class Ieee80211MgmtApQueueDropTest : public cSimpleModule ASSERT(!mgmt->hasPendingAssociation(station)); ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); + // Explicit single removal from the real DCF pending queue is a + // terminal management outcome and releases the AP-side reservation. + const MacAddress explicitlyRemovedStation("02:00:00:00:00:0b"); + mgmt->markAuthenticated(explicitlyRemovedStation); + mgmt->submitAssociationRequest(explicitlyRemovedStation); + for (int i = 0; i < 100 && queue->isEmpty(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(explicitlyRemovedStation)); + auto explicitlyRemovedPacket = queue->findPacket([](const Packet *packet) { + return packet->findTag() != nullptr; + }); + ASSERT(explicitlyRemovedPacket != nullptr); + queue->removePacket(explicitlyRemovedPacket); + take(explicitlyRemovedPacket); + delete explicitlyRemovedPacket; + for (int i = 0; i < 100 && mgmt->statuses.size() < 3; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 3); + ASSERT(mgmt->statuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(explicitlyRemovedStation)); + ASSERT(mgmt->getCommittedAssociationId(explicitlyRemovedStation) == 0); + auto explicitlyRemovedAid = mgmt->reserveAssociationId(explicitlyRemovedStation); + ASSERT(explicitlyRemovedAid != 0); + mgmt->cancelAssociationIdReservation(explicitlyRemovedStation); + + // Bulk removal invokes the callback once per independent transaction; + // both pending states and both reservations must be released. + const MacAddress bulkRemovedStation1("02:00:00:00:00:0c"); + const MacAddress bulkRemovedStation2("02:00:00:00:00:0d"); + mgmt->markAuthenticated(bulkRemovedStation1); + mgmt->markAuthenticated(bulkRemovedStation2); + mgmt->submitAssociationRequest(bulkRemovedStation1); + mgmt->submitAssociationRequest(bulkRemovedStation2); + for (int i = 0; i < 100 && queue->getNumPackets() < 2; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation2)); + queue->removeAllPackets(); + for (int i = 0; i < 100 && mgmt->statuses.size() < 5; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 5); + ASSERT(mgmt->statuses[3] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(mgmt->statuses[4] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation2)); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation1) == 0); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation2) == 0); + auto bulkAid1 = mgmt->reserveAssociationId(bulkRemovedStation1); + auto bulkAid2 = mgmt->reserveAssociationId(bulkRemovedStation2); + ASSERT(bulkAid1 != 0 && bulkAid2 != 0 && bulkAid1 != bulkAid2); + mgmt->cancelAssociationIdReservation(bulkRemovedStation1); + mgmt->cancelAssociationIdReservation(bulkRemovedStation2); + std::cout << "DCF pending-queue overflow terminal cleanup and retransmission recovery verified.\n"; } }; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 72e7714ddf5..2b66fe8b287 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -39,6 +39,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/queueing/gate/PacketGate.h" #include "inet/queueing/gate/PeriodicGate.h" #include "inet/queueing/queue/CompoundPacketQueueBase.h" @@ -630,8 +631,9 @@ class TestHcf : public Hcf void processTransmittedData(Packet *packet, const Ptr& dataHeader, AccessCategory ac) { originatorProcessTransmittedDataFrame(packet, dataHeader, ac); } void processTransmittedManagement(Packet *packet, const Ptr& managementHeader, AccessCategory ac) { originatorProcessTransmittedManagementFrame(packet, managementHeader, ac); } void dropPacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DROPPED); } - void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } + void removePacket(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason = queueing::IPacketQueue::PacketRemovalReason::REMOVED) { handlePacketRemoved(packet, reason); } void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } + bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } @@ -675,7 +677,13 @@ class TestMac : public Ieee80211Mac public: using Ieee80211Mac::receiveSignal; int numPendingRadioConfigSends = 0; + std::vector transmissionStatuses; + std::vector transmissionFrames; virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } + virtual void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + transmissionFrames.push_back(check_and_cast(object)); + transmissionStatuses.push_back(check_and_cast(details)->getStatus()); + } }; class TestEmptyFrameSequence : public IFrameSequence @@ -794,6 +802,7 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler IFrameSequence *frameSequence = nullptr; FrameSequenceContext *context = nullptr; bool running = false; + bool cancellationRequested = false; int numStartedSequences = 0; virtual ~TestFrameSequenceHandler() { @@ -812,6 +821,8 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler virtual void transmissionComplete() override {} virtual bool isSequenceRunning() override { return running; } virtual void handleStartRxTimeout() override {} + virtual void cancelFrameSequence() override { cancellationRequested = true; } + virtual void abortFrameSequence() override { running = false; } }; class TestDcaf : public Dcaf @@ -819,7 +830,11 @@ class TestDcaf : public Dcaf public: int numReleasedChannels = 0; - void configure(InProgressFrames *inProgressFrames) { this->inProgressFrames = inProgressFrames; } + void configure(InProgressFrames *inProgressFrames, queueing::IPacketQueue *pendingQueue = nullptr) { + this->inProgressFrames = inProgressFrames; + this->pendingQueue = pendingQueue; + } + virtual queueing::IPacketQueue *getPendingQueue() const override { return pendingQueue; } virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleasedChannels++; } }; @@ -830,15 +845,24 @@ class TestDcf : public Dcf for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); } - TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac) { + TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac, AckHandler *ackHandler = nullptr) { this->channelAccess = channelAccess; this->mac = mac; subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); + this->ackHandler = ackHandler; auto handler = new TestFrameSequenceHandler(); frameSequenceHandler = handler; return handler; } void grant(TestDcaf *channelAccess) { channelGranted(channelAccess); } + bool cancelManagement(uint64_t transactionId, Packet *excludedPacket) { return cancelManagementTransaction(transactionId, excludedPacket); } + void notifyRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) { handlePacketRemoved(packet, reason); } + void configureFailurePath(NonQosRecoveryProcedure *recoveryProcedure) { + this->recoveryProcedure = recoveryProcedure; + this->stationRetryCounters = new StationRetryCounters(); + } + void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } + void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } protected: virtual FrameSequenceContext *buildContext() override { @@ -870,6 +894,22 @@ static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddres return packet; } +static Packet *makeTaggedManagementPacket(const char *name, Ieee80211FrameType type, MacAddress transmitterAddress, MacAddress receiverAddress, + SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t transactionId, bool retry = false) +{ + auto header = makeShared(); + header->setType(type); + header->setTransmitterAddress(transmitterAddress); + header->setReceiverAddress(receiverAddress); + header->setSequenceNumber(sequenceNumber); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + header->setRetry(retry); + auto packet = new Packet(name, header); + packet->addTag()->setTransactionId(transactionId); + return packet; +} + static Ptr makeResponse(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, uint16_t statusCode) { auto response = makeShared(); @@ -1372,6 +1412,85 @@ ASSERT(handler.isAddbaResponsePending(peer3, 5)); delete frame; } +// Management transaction cancellation keeps active direct and RTS-protected +// frames alive until the sequence retires them, while making both frames +// ineligible immediately and suppressing terminal callbacks/retries. +for (auto coordination : { 0, 1 }) { + const uint64_t transactionId = 902 + coordination; + TestPacketQueue pendingQueue; + TestInProgressFrames inProgressFrames; + AckHandler dcfAckHandler; + TestQosAckHandler hcfAckHandler; + inProgressFrames.configure(nullptr, &dcfAckHandler, &pendingQueue); + auto directPacket = makeTaggedManagementPacket("activeManagementDirect", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(32 + coordination), 0, true, transactionId); + auto protectedPacket = makeTaggedManagementPacket("activeManagementRtsProtected", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(32 + coordination), 1, false, transactionId); + inProgressFrames.addOwnedFrame(directPacket); + inProgressFrames.addOwnedFrame(protectedPacket); + + TestFrameSequenceHandler *frameSequenceHandler; + TestMac mac; + if (coordination == 0) { + TestDcaf channelAccess; + channelAccess.configure(&inProgressFrames, &pendingQueue); + TestDcf dcf; + frameSequenceHandler = dcf.configure(&channelAccess, &mac, &dcfAckHandler); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, new NonQoSContext(nullptr), nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("activeManagementRts", makeShared()), 0)); + TestSignalListener dropListener; + dcf.subscribe(packetDroppedSignal, &dropListener); + ASSERT(dcf.cancelManagement(transactionId, nullptr)); + ASSERT(frameSequenceHandler->cancellationRequested); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 2); + ASSERT(dropListener.numSignals == 2); + ASSERT(mac.transmissionStatuses.empty()); + dcf.processFailedFrame(directPacket); + dcf.processRtsProtectionFailure(protectedPacket); + ASSERT(!directPacket->peekAtFront()->getRetry()); + ASSERT(!protectedPacket->peekAtFront()->getRetry()); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; + } + else { + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + inProgressFrames.configure(nullptr, &hcfAckHandler, &pendingQueue); + edcaf.qosAckHandler = &hcfAckHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configure(&edca); + hcf.configureMac(&mac); + frameSequenceHandler = new TestFrameSequenceHandler(); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, nullptr, nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("activeManagementRts", makeShared()), 0)); + hcf.configureFrameSequenceHandler(frameSequenceHandler); + TestSignalListener dropListener; + hcf.subscribe(packetDroppedSignal, &dropListener); + ASSERT(hcf.cancelManagement(transactionId, nullptr)); + ASSERT(frameSequenceHandler->cancellationRequested); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 2); + ASSERT(dropListener.numSignals == 2); + ASSERT(mac.transmissionStatuses.empty()); + hcf.processFailedFrame(directPacket); + hcf.processRtsProtectionFailure(protectedPacket); + ASSERT(!directPacket->peekAtFront()->getRetry()); + ASSERT(!protectedPacket->peekAtFront()->getRetry()); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; + } +} + // A transaction frame that is not referenced by an active sequence is removed // from in-progress ownership and reclaimed immediately, without entering the @@ -4317,6 +4436,331 @@ delete deferredFrames.at(0); delete dcfFrames.at(0); } +// A terminal management-frame failure cancels all fragmented siblings in DCF, +// including frames held by a different queue stage, while leaving the current +// borrowed frame and an unrelated transaction untouched. +for (auto responseType : { ST_ASSOCIATIONRESPONSE, ST_REASSOCIATIONRESPONSE }) { + TestOriginatorQosMacDataService managementDcfDataService; + TestPacketQueue managementDcfPendingQueue; + AckHandler managementDcfAckHandler; + TestInProgressFrames managementDcfInProgressFrames; + managementDcfInProgressFrames.configure(&managementDcfDataService, &managementDcfAckHandler, &managementDcfPendingQueue); + TestDcaf managementDcaf; + managementDcaf.configure(&managementDcfInProgressFrames, &managementDcfPendingQueue); + TestMac managementDcfMac; + TestDcf managementDcf; + managementDcf.configure(&managementDcaf, &managementDcfMac, &managementDcfAckHandler); + auto currentFrame = makeTaggedManagementPacket("managementDcfCurrent", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 0, true, 500 + responseType); + auto inProgressSibling = makeTaggedManagementPacket("managementDcfInProgressSibling", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 1, false, 500 + responseType); + auto pendingSibling = makeTaggedManagementPacket("managementDcfPendingSibling", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 2, false, 500 + responseType); + auto unrelatedFrame = makeTaggedManagementPacket("managementDcfUnrelated", responseType, peer1, peer2, SequenceNumberCyclic(302 + responseType), 0, false, 700 + responseType); + managementDcfInProgressFrames.addOwnedFrame(currentFrame); + managementDcfInProgressFrames.addOwnedFrame(inProgressSibling); + managementDcfPendingQueue.packets = { pendingSibling, unrelatedFrame }; + ASSERT(managementDcf.cancelManagement(500 + responseType, currentFrame)); + ASSERT(managementDcfInProgressFrames.getLength() == 1); + ASSERT(managementDcfInProgressFrames.getFrames(0) == currentFrame); + ASSERT(managementDcfPendingQueue.getNumPackets() == 1); + ASSERT(managementDcfPendingQueue.getPacket(0) == unrelatedFrame); + ASSERT(managementDcfMac.transmissionStatuses.empty()); + auto remainingDcfFrames = managementDcfInProgressFrames.releaseFrames(); + ASSERT(remainingDcfFrames.size() == 1); + delete remainingDcfFrames.front(); + managementDcfPendingQueue.removePacket(unrelatedFrame); + delete unrelatedFrame; +} + +// HCF cancellation spans every EDCA, but does not cross transaction identity +// boundaries. The active sequence's current frame is deliberately excluded. +{ + std::array managementHcfPendingQueues; + std::array managementHcfAckHandlers; + std::array managementHcfDataServices; + std::array managementHcfInProgressFrames; + std::array managementHcfEdcafs; + TestEdca managementHcfEdca; + managementHcfEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + managementHcfInProgressFrames[ac].configure(&managementHcfDataServices[ac], &managementHcfAckHandlers[ac], &managementHcfPendingQueues[ac]); + managementHcfEdcafs[ac].pendingQueue = &managementHcfPendingQueues[ac]; + managementHcfEdcafs[ac].inProgressFrames = &managementHcfInProgressFrames[ac]; + managementHcfEdcafs[ac].qosAckHandler = &managementHcfAckHandlers[ac]; + managementHcfEdcafs[ac].setAccessCategory(AccessCategory(ac)); + managementHcfEdca.edcafs[ac] = &managementHcfEdcafs[ac]; + } + managementHcfEdca.edcaf = &managementHcfEdcafs[AC_BE]; + TestMac managementHcfMac; + TestHcf managementHcf; + managementHcf.configure(&managementHcfEdca); + managementHcf.configureMac(&managementHcfMac); + auto currentFrame = makeTaggedManagementPacket("managementHcfCurrent", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 0, true, 800); + auto crossAcSibling = makeTaggedManagementPacket("managementHcfCrossAcSibling", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 1, false, 800); + auto pendingSibling = makeTaggedManagementPacket("managementHcfPendingSibling", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 2, false, 800); + auto unrelatedFrame = makeTaggedManagementPacket("managementHcfUnrelated", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(402), 0, false, 801); + managementHcfInProgressFrames[AC_BE].addOwnedFrame(currentFrame); + managementHcfInProgressFrames[AC_BK].addOwnedFrame(crossAcSibling); + managementHcfPendingQueues[AC_VO].packets = { pendingSibling }; + managementHcfPendingQueues[AC_BK].packets = { unrelatedFrame }; + ASSERT(managementHcf.cancelManagement(800, currentFrame)); + ASSERT(managementHcfInProgressFrames[AC_BE].getLength() == 1); + ASSERT(managementHcfInProgressFrames[AC_BE].getFrames(0) == currentFrame); + ASSERT(managementHcfInProgressFrames[AC_BK].getLength() == 0); + ASSERT(managementHcfPendingQueues[AC_VO].isEmpty()); + ASSERT(managementHcfPendingQueues[AC_BK].getNumPackets() == 1); + ASSERT(managementHcfPendingQueues[AC_BK].getPacket(0) == unrelatedFrame); + auto remainingHcfFrames = managementHcfInProgressFrames[AC_BE].releaseFrames(); + ASSERT(remainingHcfFrames.size() == 1); + delete remainingHcfFrames.front(); + managementHcfPendingQueues[AC_BK].removePacket(unrelatedFrame); + delete unrelatedFrame; + + auto reassociationCurrentFrame = makeTaggedManagementPacket("managementHcfReassociationCurrent", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 0, true, 802); + auto reassociationInProgressSibling = makeTaggedManagementPacket("managementHcfReassociationInProgressSibling", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 1, false, 802); + auto reassociationPendingSibling = makeTaggedManagementPacket("managementHcfReassociationPendingSibling", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 2, false, 802); + managementHcfInProgressFrames[AC_BE].addOwnedFrame(reassociationCurrentFrame); + managementHcfInProgressFrames[AC_VI].addOwnedFrame(reassociationInProgressSibling); + managementHcfPendingQueues[AC_BK].packets = { reassociationPendingSibling }; + ASSERT(managementHcf.cancelManagement(802, reassociationCurrentFrame)); + ASSERT(managementHcfInProgressFrames[AC_BE].getLength() == 1); + ASSERT(managementHcfInProgressFrames[AC_BE].getFrames(0) == reassociationCurrentFrame); + ASSERT(managementHcfInProgressFrames[AC_VI].getLength() == 0); + ASSERT(managementHcfPendingQueues[AC_BK].isEmpty()); + auto remainingReassociationFrames = managementHcfInProgressFrames[AC_BE].releaseFrames(); + ASSERT(remainingReassociationFrames.size() == 1); + delete remainingReassociationFrames.front(); +} + +// DROPPED and explicit REMOVED management departures report one terminal +// callback, including a same-token bulk removal; DEQUEUED remains a normal +// hand-off into transmission processing. +{ + TestOriginatorQosMacDataService dcfRemovalDataService; + TestPacketQueue dcfRemovalQueue; + AckHandler dcfRemovalAckHandler; + TestInProgressFrames dcfRemovalInProgressFrames; + dcfRemovalInProgressFrames.configure(&dcfRemovalDataService, &dcfRemovalAckHandler, &dcfRemovalQueue); + TestDcaf dcfRemovalDcaf; + dcfRemovalDcaf.configure(&dcfRemovalInProgressFrames, &dcfRemovalQueue); + TestMac dcfRemovalMac; + TestDcf dcfRemoval; + dcfRemoval.configure(&dcfRemovalDcaf, &dcfRemovalMac, &dcfRemovalAckHandler); + dcfRemovalQueue.subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcfRemoval); + auto droppedPacket = makeTaggedManagementPacket("dcfDroppedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(410), 0, false, 810); + dcfRemovalQueue.packets = { droppedPacket }; + dcfRemovalQueue.dropPacketFromQueue(droppedPacket); + delete droppedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 1); + ASSERT(dcfRemovalMac.transmissionStatuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + auto removedPacket = makeTaggedManagementPacket("dcfRemovedManagement", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(411), 0, false, 811); + dcfRemovalQueue.packets = { removedPacket }; + dcfRemovalQueue.removePacket(removedPacket); + delete removedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 2); + auto bulkFirst = makeTaggedManagementPacket("dcfBulkManagementFirst", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(412), 0, true, 812); + auto bulkSecond = makeTaggedManagementPacket("dcfBulkManagementSecond", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(412), 1, false, 812); + auto dequeuedPacket = makeTaggedManagementPacket("dcfDequeuedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(413), 0, false, 813); + dcfRemovalQueue.packets = { bulkFirst, bulkSecond }; + dcfRemovalQueue.removeAllPackets(); + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 3); + delete bulkFirst; + delete bulkSecond; + dcfRemovalQueue.packets = { dequeuedPacket }; + ASSERT(dcfRemovalQueue.dequeuePacket() == dequeuedPacket); + delete dequeuedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 3); + dcfRemovalQueue.unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcfRemoval); +} + +{ + std::array hcfRemovalQueues; + std::array hcfRemovalAckHandlers; + std::array hcfRemovalDataServices; + std::array hcfRemovalInProgressFrames; + std::array hcfRemovalEdcafs; + TestEdca hcfRemovalEdca; + hcfRemovalEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + hcfRemovalInProgressFrames[ac].configure(&hcfRemovalDataServices[ac], &hcfRemovalAckHandlers[ac], &hcfRemovalQueues[ac]); + hcfRemovalEdcafs[ac].pendingQueue = &hcfRemovalQueues[ac]; + hcfRemovalEdcafs[ac].inProgressFrames = &hcfRemovalInProgressFrames[ac]; + hcfRemovalEdcafs[ac].qosAckHandler = &hcfRemovalAckHandlers[ac]; + hcfRemovalEdcafs[ac].setAccessCategory(AccessCategory(ac)); + hcfRemovalEdca.edcafs[ac] = &hcfRemovalEdcafs[ac]; + } + hcfRemovalEdca.edcaf = &hcfRemovalEdcafs[AC_BE]; + TestMac hcfRemovalMac; + TestHcf hcfRemoval; + hcfRemoval.configure(&hcfRemovalEdca); + hcfRemoval.configureMac(&hcfRemovalMac); + hcfRemovalQueues[AC_VO].subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcfRemoval); + auto removedPacket = makeTaggedManagementPacket("hcfRemovedManagement", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(420), 0, false, 820); + hcfRemovalQueues[AC_VO].packets = { removedPacket }; + hcfRemovalQueues[AC_VO].removePacket(removedPacket); + delete removedPacket; + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 1); + auto bulkFirst = makeTaggedManagementPacket("hcfBulkManagementFirst", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(421), 0, true, 821); + auto bulkSecond = makeTaggedManagementPacket("hcfBulkManagementSecond", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(421), 1, false, 821); + auto untaggedPacket = new Packet("hcfBulkUntaggedManagement", makeShared()); + hcfRemovalQueues[AC_VO].packets = { bulkFirst, bulkSecond, untaggedPacket }; + hcfRemovalQueues[AC_VO].removeAllPackets(); + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 2); + delete bulkFirst; + delete bulkSecond; + delete untaggedPacket; + auto dequeuedPacket = makeTaggedManagementPacket("hcfDequeuedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(422), 0, false, 822); + hcfRemovalQueues[AC_VO].packets = { dequeuedPacket }; + ASSERT(hcfRemovalQueues[AC_VO].dequeuePacket() == dequeuedPacket); + delete dequeuedPacket; + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 2); + hcfRemovalQueues[AC_VO].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcfRemoval); +} + +// A retry-exhausted fragmented association/reassociation response cancels its +// siblings through the real DCF failure callback path. The current failed +// frame is retired by DCF, while unrelated transaction state survives. +for (int responseIndex = 0; responseIndex < 2; responseIndex++) { + auto responseType = responseIndex == 0 ? ST_ASSOCIATIONRESPONSE : ST_REASSOCIATIONRESPONSE; + constexpr int firstSequenceNumber = 440; + uint64_t transactionId = 830 + responseIndex; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + AckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestDcaf dcaf; + dcaf.configure(&inProgressFrames, &pendingQueue); + TestMac mac; + TestDcf dcf; + dcf.configure(&dcaf, &mac, &ackHandler); + dcf.configureFailurePath(&recoveryProcedure); + pendingQueue.subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcf); + + auto current = makeTaggedManagementPacket("dcfFailureCurrent", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 0, true, transactionId); + auto inProgressSibling = makeTaggedManagementPacket("dcfFailureInProgressSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 1, false, transactionId); + auto pendingSibling = makeTaggedManagementPacket("dcfFailurePendingSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 2, false, transactionId); + auto unrelated = makeTaggedManagementPacket("dcfFailureUnrelated", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex + 1), 0, false, transactionId + 100); + auto currentHeader = current->peekAtFront(); + auto inProgressSiblingHeader = inProgressSibling->peekAtFront(); + inProgressFrames.addOwnedFrame(current); + inProgressFrames.addOwnedFrame(inProgressSibling); + pendingQueue.packets = { pendingSibling, unrelated }; + ackHandler.frameGotInProgress(currentHeader); + ackHandler.frameGotInProgress(inProgressSiblingHeader); + ackHandler.processTransmittedDataOrMgmtFrame(currentHeader); + ackHandler.processTransmittedDataOrMgmtFrame(inProgressSiblingHeader); + recoveryProcedure.addShortRetry(inProgressSiblingHeader, 2); + + dcf.processFailedFrame(current); + + ASSERT(mac.transmissionStatuses.size() == 1); + ASSERT(mac.transmissionStatuses.front() == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + ASSERT(mac.transmissionFrames.front() == current); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(pendingQueue.getNumPackets() == 1); + ASSERT(pendingQueue.getPacket(0) == unrelated); + ASSERT(ackHandler.getAckStatus(currentHeader) == AckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(ackHandler.getAckStatus(inProgressSiblingHeader) == AckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(!recoveryProcedure.hasRetry(currentHeader)); + ASSERT(!recoveryProcedure.hasRetry(inProgressSiblingHeader)); + auto retiredFrames = inProgressFrames.releaseFrames(); + ASSERT(retiredFrames == std::vector({ current })); + delete current; + pendingQueue.packets.clear(); + delete unrelated; + pendingQueue.unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcf); +} + +// The same retry-exhaustion path in HCF spans EDCA queues, clears each +// sibling's QoS ACK/retry bookkeeping, and emits one terminal callback for +// both association response variants. +for (int responseIndex = 0; responseIndex < 2; responseIndex++) { + auto responseType = responseIndex == 0 ? ST_ASSOCIATIONRESPONSE : ST_REASSOCIATIONRESPONSE; + constexpr int firstSequenceNumber = 450; + uint64_t transactionId = 840 + responseIndex; + std::array pendingQueues; + std::array ackHandlers; + std::array dataServices; + std::array inProgressFrames; + std::array edcafs; + TestEdca edca; + edca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + inProgressFrames[ac].configure(&dataServices[ac], &ackHandlers[ac], &pendingQueues[ac]); + edcafs[ac].pendingQueue = &pendingQueues[ac]; + edcafs[ac].inProgressFrames = &inProgressFrames[ac]; + edcafs[ac].qosAckHandler = &ackHandlers[ac]; + edcafs[ac].setAccessCategory(AccessCategory(ac)); + edca.edcafs[ac] = &edcafs[ac]; + } + edca.edcaf = &edcafs[AC_BE]; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestHcf hcf; + hcf.configure(&edca); + hcf.configureMac(&mac); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); + + auto current = makeTaggedManagementPacket("hcfFailureCurrent", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 0, true, transactionId); + auto inProgressSibling = makeTaggedManagementPacket("hcfFailureInProgressSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 1, false, transactionId); + auto pendingSibling = makeTaggedManagementPacket("hcfFailurePendingSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 2, false, transactionId); + auto unrelated = makeTaggedManagementPacket("hcfFailureUnrelated", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex + 1), 0, false, transactionId + 100); + auto currentHeader = current->peekAtFront(); + auto inProgressSiblingHeader = inProgressSibling->peekAtFront(); + inProgressFrames[AC_BE].addOwnedFrame(current); + inProgressFrames[AC_VI].addOwnedFrame(inProgressSibling); + pendingQueues[AC_VO].packets = { pendingSibling }; + pendingQueues[AC_BK].packets = { unrelated }; + ackHandlers[AC_BE].frameGotInProgress(currentHeader); + ackHandlers[AC_VI].frameGotInProgress(inProgressSiblingHeader); + hcf.processTransmittedManagement(current, currentHeader, AC_BE); + hcf.processTransmittedManagement(inProgressSibling, inProgressSiblingHeader, AC_VI); + ASSERT(ackHandlers[AC_BE].getMgmtOrNonQoSAckStatus(currentHeader) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + ASSERT(ackHandlers[AC_VI].getMgmtOrNonQoSAckStatus(inProgressSiblingHeader) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + recoveryProcedure.addShortRetry(inProgressSiblingHeader, 2); + + hcf.processFailedFrame(current); + + ASSERT(mac.transmissionStatuses.size() == 1); + ASSERT(mac.transmissionStatuses.front() == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + ASSERT(mac.transmissionFrames.front() == current); + ASSERT(inProgressFrames[AC_BE].getLength() == 0); + ASSERT(inProgressFrames[AC_BE].getNumDroppedFrames() == 1); + ASSERT(inProgressFrames[AC_VI].getLength() == 0); + ASSERT(pendingQueues[AC_VO].isEmpty()); + ASSERT(pendingQueues[AC_BK].getNumPackets() == 1); + ASSERT(pendingQueues[AC_BK].getPacket(0) == unrelated); + ASSERT(ackHandlers[AC_BE].getMgmtOrNonQoSAckStatus(currentHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(ackHandlers[AC_VI].getMgmtOrNonQoSAckStatus(inProgressSiblingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(!recoveryProcedure.hasRetry(currentHeader)); + ASSERT(!recoveryProcedure.hasRetry(inProgressSiblingHeader)); + auto retiredFrames = inProgressFrames[AC_BE].releaseFrames(); + ASSERT(retiredFrames == std::vector({ current })); + delete current; + pendingQueues[AC_VO].packets.clear(); + pendingQueues[AC_BK].packets.clear(); + delete unrelated; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From 18557831734f0431dc05f1c2c3beaa6e5534cf18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:43:19 +0200 Subject: [PATCH 33/91] ieee80211: add+fix: protect Block Ack teardown generations A delayed or retried DELBA must not terminate a replacement agreement for the same peer and TID. Carry the sender-local generation through fragments and retries, and make acknowledged, aborted and cancelled teardown outcomes explicit. Validation: diagnostic debug and release builds; 10 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.add+change.fix | test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 28 +- doc/src/migration-guide/index.rst | 16 + .../Ieee80211BlockAckAgreementTag.msg | 17 + .../OriginatorBlockAckAgreementHandler.cc | 69 +- .../OriginatorBlockAckAgreementHandler.h | 2 + .../blockack/RecipientBlockAckAgreement.cc | 5 +- .../mac/blockack/RecipientBlockAckAgreement.h | 4 +- .../RecipientBlockAckAgreementHandler.cc | 110 +++- .../RecipientBlockAckAgreementHandler.h | 11 +- .../IBlockAckAgreementHandlerCallback.h | 6 + .../IRecipientBlockAckAgreementHandler.h | 9 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 135 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 3 + tests/unit/Ieee80211AddbaTransaction_1.test | 597 +++++++++++++++++- 14 files changed, 936 insertions(+), 76 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg diff --git a/WHATSNEW b/WHATSNEW index a431aff0e42..2d1373b2091 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -339,6 +339,9 @@ Notable backward incompatible changes are the following: isDelbaPending(). Custom IFrameSequenceHandler implementations must explicitly implement cancelFrameSequence() and abortFrameSequence(). + Custom agreement callbacks must implement cancelBlockAckTeardown(); + recipient handlers must implement processAcknowledgedDelba(), + processAbortedDelba(), getPendingTeardownGenerationId() and isDelbaPending(). IOriginatorBlockAckAgreementHandler::processReceivedDelba(), IOriginatorBlockAckAgreementHandler::processTransmittedDelba(), and @@ -347,20 +350,27 @@ Notable backward incompatible changes are the following: now returns a typed outcome containing the established agreement and, when local policy vetoes a successful response, its immediately terminated local agreement and a best-effort initiator DELBA for Hcf to enqueue after emitting - the Added and Deleted signals. Such DELBAs carry the ADDBA transaction identity - and ordinary data continues with Normal Ack. They remain eligible through the + the Added and Deleted signals. Such locally generated DELBAs carry + sender-local agreement-generation metadata (the originator reuses its ADDBA + transaction identity), and ordinary data continues with Normal Ack. They remain + eligible through the final fragment; aborting one fragment cancels its siblings. After retry backoff, a replacement ADDBA setup invalidates an older queued DELBA, so an unreported disposal cannot suppress setup indefinitely and a delayed frame cannot terminate a newer peer/TID agreement. The replacement setup reports that obsolete identity so Hcf also removes all of its queued or in-progress packets. - Originator DELBA transmission handlers now receive the full Packet so this - identity is retained. A transaction-tagged initiator DELBA remains eligible - across MAC retries and is retired only when its final fragment is acknowledged - or the transaction is terminally aborted. Custom handler implementations and - callers must adopt processAcknowledgedDelba() and the boolean - processAbortedDelba() outcome; processTransmittedDelba() still returns the - agreement removed by an untagged DELBA, or null when none was removed. + Originator and recipient DELBA transmission handlers now receive the full + Packet so sender-local agreement-generation metadata is retained through + fragmentation and MAC retries. This changes + IRecipientBlockAckAgreementHandler::processTransmittedDelba() from accepting + only a DELBA header to accepting Packet*; custom handler implementations and + callers must migrate to the Packet form. A locally tagged DELBA carrying + agreement-generation metadata remains eligible across MAC retries and is + retired only when its final fragment is acknowledged or the teardown is + terminally aborted. Custom handler + implementations and callers must adopt processAcknowledgedDelba() and the + boolean processAbortedDelba() outcome; processTransmittedDelba() still returns + the agreement removed by an untagged DELBA, or null when none was removed. OriginatorBlockAckAgreementPolicy now exposes the new `addbaResponseTimeout` and `addbaRetryBackoff` NED parameters. C++ implementations of diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index be2f3d4c376..581e4b62efb 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -140,6 +140,22 @@ ownership with the caller: cancellation must not delete a packet still borrowed in-flight transmission or active sequence. Superseded AP management transactions must retire their queued siblings and terminal outcome exactly once. +Generation-Aware Block Ack Teardown +----------------------------------- + +Custom agreement callbacks must implement ``cancelBlockAckTeardown()``. Match the role, +peer, TID and generation so cancellation cannot affect a replacement agreement. Preserve +``Ieee80211BlockAckAgreementTag::generationId`` on locally generated DELBA fragments and +retries; the role, peer and TID come from DELBA and its processing context. + +Recipient agreement handlers must explicitly implement ``isDelbaPending()``, +``processAcknowledgedDelba()``, ``processAbortedDelba()`` and +``getPendingTeardownGenerationId()``. Update ``processTransmittedDelba()`` +implementations and callers to the full ``Packet *`` form where required, so local +generation metadata remains available. Keep teardown pending until its final fragment is +acknowledged or the exchange terminates; do not retire a replacement generation from a +stale completion. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg new file mode 100644 index 00000000000..01a57c8aa59 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg @@ -0,0 +1,17 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; + +namespace inet::ieee80211; + +// Identifies one local Block Ack agreement generation across packet transformations. +// This metadata is not transmitted as part of the DELBA frame. +class Ieee80211BlockAckAgreementTag extends TagBase +{ + uint64_t generationId; +} diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 89606d3fcb7..2b1e4bbbd8a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -11,6 +11,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -111,6 +112,7 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(agreement->getTransactionId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes } } @@ -307,11 +309,16 @@ bool OriginatorBlockAckAgreementHandler::isDelbaPending(const Packet *packet, co { if (!delba->getInitiator()) return true; - auto transactionTag = packet->findTag(); - if (transactionTag == nullptr) - return true; - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - return it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId(); + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto generationId = agreementTag->getGenerationId(); + auto agreementIt = blockAckAgreements.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (agreementIt != blockAckAgreements.end()) + return agreementIt->second->getTransactionId() == generationId; + auto teardownIt = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return teardownIt != pendingTeardownTransactionIds.end() && teardownIt->second == generationId; + } + return true; } void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -344,18 +351,23 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: // likewise cannot tear down the agreement before its final fragment. if (delba->getMoreFragments()) return nullptr; - auto transactionTag = packet->findTag(); - if (transactionTag != nullptr) { - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto generationId = agreementTag->getGenerationId(); + auto teardownIt = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (teardownIt != pendingTeardownTransactionIds.end() && teardownIt->second == generationId) return nullptr; - // IEEE Std 802.11-2024, 11.5.3.2: teardown is performed by - // transmitting DELBA. IEEE Std 802.11-2024, 10.23.2.12.1 and - // 10.3.4.4 require unsuccessful MMPDU attempts to be retried until - // success or the applicable retry limit. Keep the local transaction - // live across ordinary MAC retries and retire it only after the final - // fragment is acknowledged or the frame is terminally aborted. - return nullptr; + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + if (agreement == nullptr || agreement->getTransactionId() != generationId) + return nullptr; + bool cancelPendingTransaction = agreement->isPending(); + std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(generationId, nullptr); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return terminatedAgreement; } auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); @@ -372,15 +384,15 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator() || delba->getMoreFragments()) return false; - auto transactionTag = packet->findTag(); - if (transactionTag == nullptr) + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) return false; auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + if (it == pendingTeardownTransactionIds.end() || it->second != agreementTag->getGenerationId()) return false; auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelAddbaTransaction(transactionId, packet); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); return true; } @@ -389,13 +401,13 @@ bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBl auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) return false; - auto transactionTag = packet->findTag(); - if (transactionTag != nullptr) { + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId()) { + if (it != pendingTeardownTransactionIds.end() && it->second == agreementTag->getGenerationId()) { auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelAddbaTransaction(transactionId, packet); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); return true; } } @@ -408,10 +420,19 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: auto agreement = getAgreement(delba->getTransmitterAddress(), delba->getTid()); bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); + auto pendingTeardownIt = pendingTeardownTransactionIds.find(agreementId); + auto pendingTeardownTransactionId = pendingTeardownIt == pendingTeardownTransactionIds.end() ? 0 : pendingTeardownIt->second; + if (pendingTeardownIt != pendingTeardownTransactionIds.end()) + pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); + if (pendingTeardownTransactionId != 0) + callback->cancelBlockAckTeardown(true, delba->getTransmitterAddress(), delba->getTid(), pendingTeardownTransactionId, nullptr); + if (terminatedAgreement != nullptr) + callback->cancelBlockAckTeardown(true, delba->getTransmitterAddress(), delba->getTid(), terminatedAgreement->getTransactionId(), nullptr); return terminatedAgreement; } return nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 9933ccadf92..0eb290334c6 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -22,6 +22,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; std::map, simtime_t> addbaRetryDeadlines; + // A tagged local DELBA remains eligible after its agreement is removed + // until the final fragment is acknowledged or terminally aborted. std::map, uint64_t> pendingTeardownTransactionIds; uint8_t nextDialogToken = 1; uint64_t nextTransactionId = 1; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc index d93711d2839..1d3f447b9b4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -12,10 +12,11 @@ namespace inet { namespace ieee80211 { -RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime) : +RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime, uint64_t generationId) : startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), - blockAckTimeoutValue(lastUsedTime) + blockAckTimeoutValue(lastUsedTime), + generationId(generationId) { calculateExpirationTime(); blockAckRecord = new BlockAckRecord(originatorAddress, tid); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index aa0c9763e9c..6bb5da30c25 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -22,9 +22,10 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; simtime_t expirationTime = -1; + uint64_t generationId = 0; public: - RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue); + RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue, uint64_t generationId = 0); virtual ~RecipientBlockAckAgreement() { delete blockAckRecord; } virtual void blockAckPolicyFrameReceived(const Ptr& header); @@ -33,6 +34,7 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } + virtual uint64_t getGenerationId() const { return generationId; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } virtual simtime_t getExpirationTime() { return expirationTime; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 39528886313..9c6d41c29f8 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -8,6 +8,8 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" namespace inet { namespace ieee80211 { @@ -58,6 +60,7 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(agreement->getGenerationId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes } } @@ -79,6 +82,13 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } +uint64_t RecipientBlockAckAgreementHandler::allocateAgreementGenerationId() +{ + if (nextAgreementGenerationId == 0) + throw cRuntimeError("Block Ack agreement generation ID exhausted"); + return nextAgreementGenerationId++; +} + const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted) { auto addbaResponse = makeShared(); @@ -138,9 +148,18 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAd // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.3: accepting the // request establishes or modifies the recipient agreement when the // successful response is formed; transmission is not a state gate. - agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue()); + auto pendingTeardownIt = pendingTeardownGenerationIds.find(id); + auto pendingTeardownGenerationId = pendingTeardownIt == pendingTeardownGenerationIds.end() ? 0 : pendingTeardownIt->second; + if (pendingTeardownIt != pendingTeardownGenerationIds.end()) + pendingTeardownGenerationIds.erase(pendingTeardownIt); + if (pendingTeardownGenerationId != 0 && agreementHandlerCallback != nullptr) + agreementHandlerCallback->cancelBlockAckTeardown(false, id.first, id.second, pendingTeardownGenerationId, nullptr); + auto generationId = allocateAgreementGenerationId(); + agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue(), generationId); auto it = blockAckAgreements.find(id); if (it != blockAckAgreements.end()) { + if (agreementHandlerCallback != nullptr && (pendingTeardownGenerationId == 0 || pendingTeardownGenerationId != it->second->getGenerationId())) + agreementHandlerCallback->cancelBlockAckTeardown(false, id.first, id.second, it->second->getGenerationId(), nullptr); delete it->second; it->second = agreement; } @@ -170,19 +189,102 @@ void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +bool RecipientBlockAckAgreementHandler::isDelbaPending(const Packet *packet, const Ptr& delba) const +{ + if (delba->getInitiator()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return true; + auto it = blockAckAgreements.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it != blockAckAgreements.end()) + return it->second->getGenerationId() == agreementTag->getGenerationId(); + auto teardownIt = pendingTeardownGenerationIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return teardownIt != pendingTeardownGenerationIds.end() && teardownIt->second == agreementTag->getGenerationId(); +} + +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet) { + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return nullptr; // IEEE Std 802.11-2024, 10.4 and 11.5.3.5: the DELBA MMPDU has not // been transmitted while a later fragment is still outstanding. if (delba->getMoreFragments()) return nullptr; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto agreement = getAgreement(delba->getTid(), delba->getReceiverAddress()); + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + if (agreement != nullptr) { + if (agreement->getGenerationId() != agreementTag->getGenerationId()) + return nullptr; + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); + return terminatedAgreement; + } + auto teardownIt = pendingTeardownGenerationIds.find(agreementId); + if (teardownIt == pendingTeardownGenerationIds.end() || teardownIt->second != agreementTag->getGenerationId()) + return nullptr; + return nullptr; + } return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } +bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator() || delba->getMoreFragments()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return false; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return true; +} + +bool RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return false; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return true; +} + +uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const +{ + auto it = pendingTeardownGenerationIds.find(std::make_pair(originatorAddr, tid)); + return it == pendingTeardownGenerationIds.end() ? 0 : it->second; +} + std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - return std::unique_ptr(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { + auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); + auto pendingTeardownIt = pendingTeardownGenerationIds.find(agreementId); + if (pendingTeardownIt != pendingTeardownGenerationIds.end()) + pendingTeardownGenerationIds.erase(pendingTeardownIt); + std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + return terminatedAgreement; + } return nullptr; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index f635b973868..3188e24b6d6 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -27,11 +27,16 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; std::map, Ptr> lastAddbaResponses; + // A tagged local DELBA remains eligible after its agreement is removed + // until the final fragment is acknowledged or terminally aborted. + std::map, uint64_t> pendingTeardownGenerationIds; + uint64_t nextAgreementGenerationId = 1; protected: virtual RecipientBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); + virtual uint64_t allocateAgreementGenerationId(); virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); @@ -41,10 +46,14 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet) override; + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; + virtual uint64_t getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const override; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index d83044b63a2..77c842be775 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -9,6 +9,8 @@ #define __INET_IBLOCKACKAGREEMENTHANDLERCALLBACK_H #include "inet/common/INETDefs.h" +#include "inet/linklayer/common/MacAddress.h" +#include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" namespace inet { @@ -24,6 +26,10 @@ class INET_API IBlockAckAgreementHandlerCallback virtual void scheduleInactivityTimer(simtime_t timeout) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; + // Removes queued siblings of a sender-local DELBA without assuming that + // the frame is still removable from the active frame sequence. The + // agreement owner remains responsible for rejecting stale packets. + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 875d6f0c557..ded64d55b0a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -19,6 +19,9 @@ #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { + +class Packet; + namespace ieee80211 { class INET_API IRecipientBlockAckAgreementHandler @@ -29,11 +32,15 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet) = 0; + virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; + virtual bool processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; + virtual uint64_t getPendingTeardownGenerationId(Tid, MacAddress) const = 0; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index fe16bb35319..6630a4b5b15 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" @@ -73,8 +74,11 @@ void Hcf::initialize(int stage) originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { if (auto addbaReq = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); - if (auto delba = findFragmentedActionContext(packet)) - return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + if (auto delba = findFragmentedActionContext(packet)) { + if (delba->getInitiator()) + return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + return recipientBlockAckAgreementHandler->isDelbaPending(packet, delba); + } auto dataHeader = dynamicPtrCast(packet->peekAtFront()); // Hold this peer/TID while its ADDBA response is pending so no // already-sequenced MPDU can precede the advertised SSN. The @@ -146,12 +150,18 @@ bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) { - if (originatorBlockAckAgreementHandler) { - auto delba = findFragmentedActionContext(packet); - if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { - rebuildPendingFrameEligibility(); - return true; - } + if (originatorBlockAckAgreementHandler == nullptr && recipientBlockAckAgreementHandler == nullptr) + return false; + auto delba = findFragmentedActionContext(packet); + if (delba == nullptr) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr && blockAckTeardownsBeingCancelled.find(std::make_tuple(delba->getInitiator(), delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId())) != blockAckTeardownsBeingCancelled.end()) + return false; + bool aborted = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + if (aborted) { + rebuildPendingFrameEligibility(); + return true; } return false; } @@ -479,6 +489,71 @@ void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) } } +void Hcf::cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) +{ + Enter_Method("cancelBlockAckTeardown"); + if (edca == nullptr) + return; + auto teardownId = std::make_tuple(initiator, peerAddress, tid, generationId); + bool outerCancellation = blockAckTeardownsBeingCancelled.insert(teardownId).second; + auto belongsToTeardown = [initiator, peerAddress, tid, generationId, excludedPacket](Packet *packet) { + if (packet == excludedPacket) + return false; + auto delba = findFragmentedActionContext(packet); + auto agreementTag = packet->findTag(); + return delba != nullptr && agreementTag != nullptr && delba->getInitiator() == initiator && + delba->getReceiverAddress() == peerAddress && delba->getTid() == tid && + agreementTag->getGenerationId() == generationId; + }; + bool removedPacket = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + if (pendingQueue != nullptr) { + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTeardown(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + removedPacket = true; + } + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + if (inProgressFrames != nullptr) { + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTeardown(packet) && !isPacketReferencedByCurrentFrameSequence(packet)) { + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + auto managementRecoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (managementRecoveryProcedure != nullptr) + managementRecoveryProcedure->discardFrame(packet, header); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + removedPacket = true; + } + } + } + } + if (removedPacket && originatorDataService != nullptr) { + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } + if (outerCancellation) + blockAckTeardownsBeingCancelled.erase(teardownId); +} + void Hcf::processLowerFrame(Packet *packet, const Ptr& header) { Enter_Method("processLowerFrame(%s)", packet->getName()); @@ -702,6 +777,8 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { @@ -719,7 +796,7 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtraddTag()->setTransactionId(response.teardownTransactionId); + delbaPacket->addTag()->setGenerationId(response.teardownTransactionId); processMgmtFrame(delbaPacket, response.teardownDelba); } resumeEligibleChannelAccess(); @@ -728,19 +805,33 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrgetInitiator()) { + auto pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto remainingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); if (agreement != nullptr) { recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); emit(blockAckAgreementDeletedSignal, agreement.get()); + cancelBlockAckTeardown(false, delba->getTransmitterAddress(), delba->getTid(), agreement->getGenerationId(), nullptr); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } + bool pendingTeardownRetired = pendingTeardownGenerationId != 0 && remainingTeardownGenerationId != pendingTeardownGenerationId; + if (pendingTeardownRetired && (agreement == nullptr || agreement->getGenerationId() != pendingTeardownGenerationId)) + cancelBlockAckTeardown(false, delba->getTransmitterAddress(), delba->getTid(), pendingTeardownGenerationId, nullptr); + if (pendingTeardownRetired && agreement == nullptr && edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } else { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); auto agreement = originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy, this); if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid())) rebuildPendingFrameEligibility(); - if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) - emit(blockAckAgreementDeletedSignal, agreement.get()); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } resumeEligibleChannelAccess(); } } @@ -890,17 +981,24 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); if (wasPending) rebuildPendingFrameEligibility(); - if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) - emit(blockAckAgreementDeletedSignal, agreement.get()); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } } else { - auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet); if (agreement != nullptr) { // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient // resources are released whether the recipient transmitted or // received DELBA. The reorder window is such a resource. recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); emit(blockAckAgreementDeletedSignal, agreement.get()); + cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), agreement->getGenerationId(), packet); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } } } @@ -1062,7 +1160,8 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { - if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) + bool acknowledged = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this); + if (acknowledged) rebuildPendingFrameEligibility(); } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); @@ -1076,8 +1175,10 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrisAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); auto obsoleteTeardownTransactionId = originatorBlockAckAgreementHandler->processAcknowledgedDataFrame(lastTransmittedPacket, dataHeader, originatorBlockAckAgreementPolicy, this); - if (obsoleteTeardownTransactionId != 0) - cancelAddbaTransaction(obsoleteTeardownTransactionId, nullptr); + if (obsoleteTeardownTransactionId != 0) { + cancelBlockAckTeardown(true, dataHeader->getReceiverAddress(), dataHeader->getTid(), obsoleteTeardownTransactionId, nullptr); + rebuildPendingFrameEligibility(); + } if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) rebuildPendingFrameEligibility(); } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 53a53e2361b..ae3c2a9ab9f 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" @@ -122,6 +123,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: std::set completedManagementTransactions; eventnumber_t completedManagementTransactionsEventNumber = -1; std::set cancelledManagementTransactions; + std::set> blockAckTeardownsBeingCancelled; // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -197,6 +199,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void scheduleInactivityTimer(simtime_t timeout) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 2b66fe8b287..dbd732ff9ff 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -16,6 +16,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" @@ -107,6 +108,9 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler simtime_t addbaDeadline = SIMTIME_MAX; std::vector cancelledTransactionIds; std::vector excludedPackets; + std::vector cancelledTeardownGenerationIds; + std::vector excludedTeardownPackets; + std::vector cancelledTeardownInitiators; virtual ~TestCallback() { for (auto packet : managementPackets) delete packet; } void forgetManagementPacket(Packet *packet) { managementPackets.erase(std::remove(managementPackets.begin(), managementPackets.end(), packet), managementPackets.end()); } @@ -122,6 +126,11 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler cancelledTransactionIds.push_back(transactionId); excludedPackets.push_back(excludedPacket); } + virtual void cancelBlockAckTeardown(bool initiator, MacAddress, Tid, uint64_t generationId, Packet *excludedPacket) override { + cancelledTeardownInitiators.push_back(initiator); + cancelledTeardownGenerationIds.push_back(generationId); + excludedTeardownPackets.push_back(excludedPacket); + } }; class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler @@ -129,7 +138,7 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler public: using OriginatorBlockAckAgreementHandler::buildAddbaRequest; void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { - auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, 1); + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, nextTransactionId++); agreement->setIsAddbaResponseReceived(true); blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; } @@ -581,6 +590,8 @@ class TestHcf : public Hcf int numProcessedDroppedSetupFrames = 0; int numResumedEligibleChannelAccess = 0; int numCancelledAddbaTransactions = 0; + int numCancelledBlockAckTeardowns = 0; + std::vector cancelledBlockAckTeardownGenerationIds; int numRebuildEligibilityCalls = 0; int numTransmittedControlResponses = 0; bool droppedSetupCancelled = false; @@ -635,6 +646,7 @@ class TestHcf : public Hcf void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void cancelTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket = nullptr) { cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } @@ -658,6 +670,7 @@ class TestHcf : public Hcf } virtual bool processDroppedBlockAckSetupFrame(Packet *packet) override { numProcessedDroppedSetupFrames++; return delegateDroppedSetupHandling ? Hcf::processDroppedBlockAckSetupFrame(packet) : droppedSetupCancelled; } virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { numCancelledAddbaTransactions++; Hcf::cancelAddbaTransaction(transactionId, excludedPacket); } + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override { numCancelledBlockAckTeardowns++; cancelledBlockAckTeardownGenerationIds.push_back(generationId); Hcf::cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } virtual void scheduleInactivityTimer(simtime_t) override {} @@ -890,7 +903,21 @@ static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddres delba->setFragmentNumber(fragmentNumber); delba->setMoreFragments(moreFragments); auto packet = new Packet(name, delba); - packet->addTag()->setTransactionId(transactionId); + packet->addTag()->setGenerationId(transactionId); + return packet; +} + +static Packet *makeTaggedRecipientDelbaPacket(const char *name, MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t generationId) +{ + auto delba = makeShared(); + delba->setReceiverAddress(receiverAddress); + delba->setTid(tid); + delba->setInitiator(false); + delba->setSequenceNumber(sequenceNumber); + delba->setFragmentNumber(fragmentNumber); + delba->setMoreFragments(moreFragments); + auto packet = new Packet(name, delba); + packet->addTag()->setGenerationId(generationId); return packet; } @@ -1021,8 +1048,8 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); finalDelba->setMoreFragments(false); Packet firstDelbaPacket("firstDelbaFragment", firstDelba); Packet finalDelbaPacket("finalDelbaFragment", finalDelba); - firstDelbaPacket.addTag()->setTransactionId(77); - finalDelbaPacket.addTag()->setTransactionId(77); + firstDelbaPacket.addTag()->setGenerationId(77); + finalDelbaPacket.addTag()->setGenerationId(77); fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 77); ASSERT(fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&firstDelbaPacket, &fragmentedDelbaCallback) == nullptr); @@ -1035,15 +1062,15 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); ASSERT(!fragmentedDelbaHandler.processAcknowledgedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); ASSERT(fragmentedDelbaHandler.processAcknowledgedDelba(&finalDelbaPacket, &fragmentedDelbaCallback)); ASSERT(!fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); - ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 77); - ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &finalDelbaPacket); + ASSERT(fragmentedDelbaCallback.cancelledTeardownGenerationIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &finalDelbaPacket); fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); - firstDelbaPacket.getTagForUpdate()->setTransactionId(78); + firstDelbaPacket.getTagForUpdate()->setGenerationId(78); ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); - ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 78); - ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &firstDelbaPacket); + ASSERT(fragmentedDelbaCallback.cancelledTeardownGenerationIds.back() == 78); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &firstDelbaPacket); } // IEEE Std 802.11-2024, 10.23.2.12.1 and 11.5.3.2: the HCF retry path keeps a @@ -1132,7 +1159,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); completeDelba->setInitiator(true); completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); - completeDelbaPacket->addTag()->setTransactionId(transactionId); + completeDelbaPacket->addTag()->setGenerationId(transactionId); completeDelbaPacket->insertAtBack(makeShared()); Fragmentation fragmentation; auto delbaFragments = fragmentation.fragmentFrame(completeDelbaPacket, { 4, 2 }); @@ -1153,13 +1180,13 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); Packet ackPacket("transactionalDelbaAck", makeShared()); hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); ASSERT(!originatorHandler->isDelbaPending(finalPacket, findFragmentedActionContext(finalPacket))); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); ASSERT(!originatorHandler->processAcknowledgedDelba(finalPacket, &hcf)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; @@ -1210,13 +1237,13 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); hcf.processFailedFrame(finalPacket); ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); ASSERT(!originatorHandler->processAbortedDelba(finalPacket, &hcf)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; @@ -1282,7 +1309,7 @@ ASSERT(handler.processReceivedAddbaResp(success, &originatorPolicy, &callback).e ASSERT(vetoDelba->getTid() == 6); ASSERT(vetoDelba->getReasonCode() == RC_END_BA); Packet vetoDelbaPacket("vetoDelba", vetoDelba); - vetoDelbaPacket.addTag()->setTransactionId(vetoResponseOutcome.teardownTransactionId); + vetoDelbaPacket.addTag()->setGenerationId(vetoResponseOutcome.teardownTransactionId); ASSERT(vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); ASSERT(vetoHandler.getRetryDeadline(peer3, 6) > simTime()); @@ -2710,8 +2737,8 @@ ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateA auto heldTeardownRetryHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(212)); auto obsoleteHeldTeardownTransactionId = heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownRetryTrigger, heldTeardownRetryHeader, &heldTeardownOriginatorPolicy, &heldTeardownRetryCallback); ASSERT(heldTeardownRetryCallback.managementPacket != nullptr); - ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getTransactionId()); - heldTeardownHcf.cancelTransaction(obsoleteHeldTeardownTransactionId); + ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getGenerationId()); + heldTeardownHcf.cancelTeardown(true, peer1, 7, obsoleteHeldTeardownTransactionId); ASSERT(heldTeardownQueue.getNumPackets() == 1); ASSERT(heldTeardownQueue.getPacket(0) == heldDataPacket); heldTeardownHcf.rebuildEligibilityIndex(); @@ -4761,6 +4788,542 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); } +// An originator inactivity DELBA retains the agreement transaction identity. +// Once that agreement is replaced, every stale fragment is rejected while a +// final DELBA carrying the current generation removes the replacement once. +{ + TestOriginatorHandler expiryHandler; + TestOriginatorPolicy expiryPolicy; + TestCallback setupCallback; + Packet trigger("originatorExpiryTrigger"); + auto triggerHeader = makeQosHeader(peer1, 9, SequenceNumberCyclic(300)); + expiryHandler.processAcknowledgedDataFrame(&trigger, triggerHeader, &expiryPolicy, &setupCallback); + auto setupRequest = dynamicPtrCast(setupCallback.managementHeader); + expiryHandler.processTransmittedAddbaReq(setupCallback.managementPacket, setupRequest, &expiryPolicy, &setupCallback); + auto setupResponse = makeResponse(peer1, 9, setupRequest->getDialogToken(), 0); + setupResponse->setBlockAckTimeoutValue(1); + auto establishedOutcome = expiryHandler.processReceivedAddbaResp(setupResponse, &expiryPolicy, &setupCallback); + auto firstAgreement = establishedOutcome.establishedAgreement; + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getTransactionId(); + firstAgreement->setBlockAckTimeoutValue(1); + firstAgreement->calculateExpirationTime(); + wait(1); + TestCallback expiryCallback; + expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + auto stalePacket = expiryCallback.managementPacket; + auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); + ASSERT(stalePacket != nullptr && staleDelba != nullptr); + ASSERT(stalePacket->getTag()->getGenerationId() == firstGenerationId); + + auto receivedDelba = makeShared(); + receivedDelba->setTransmitterAddress(peer1); + receivedDelba->setTid(9); + auto terminatedFirstAgreement = expiryHandler.processReceivedDelba(receivedDelba, &expiryPolicy, &expiryCallback); + ASSERT(terminatedFirstAgreement != nullptr); + TestCallback replacementCallback; + Packet replacementTrigger("originatorReplacementTrigger"); + auto replacementHeader = makeQosHeader(peer1, 9, SequenceNumberCyclic(301)); + expiryHandler.processAcknowledgedDataFrame(&replacementTrigger, replacementHeader, &expiryPolicy, &replacementCallback); + auto replacementRequest = dynamicPtrCast(replacementCallback.managementHeader); + expiryHandler.processTransmittedAddbaReq(replacementCallback.managementPacket, replacementRequest, &expiryPolicy, &replacementCallback); + auto replacementResponse = makeResponse(peer1, 9, replacementRequest->getDialogToken(), 0); + auto replacementOutcome = expiryHandler.processReceivedAddbaResp(replacementResponse, &expiryPolicy, &replacementCallback); + auto secondAgreement = replacementOutcome.establishedAgreement; + ASSERT(secondAgreement != nullptr); + auto secondGenerationId = secondAgreement->getTransactionId(); + ASSERT(secondGenerationId != firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(stalePacket, staleDelba)); + + auto staleFragmentBody = staticPtrCast(staleDelba->dupShared()); + staleFragmentBody->setSequenceNumber(SequenceNumberCyclic(302)); + auto staleFragmentSource = new Packet("originatorStaleDelbaSource", staleFragmentBody); + staleFragmentSource->addTag()->setGenerationId(firstGenerationId); + staleFragmentSource->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto staleFragments = fragmentation.fragmentFrame(staleFragmentSource, { 4, 2 }); + for (auto fragment : *staleFragments) + ASSERT(fragment->getTag()->getGenerationId() == firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(staleFragments->at(0), findFragmentedActionContext(staleFragments->at(0)))); + ASSERT(expiryHandler.processTransmittedDelba(staleFragments->at(1), &expiryCallback) == nullptr); + ASSERT(expiryHandler.getAgreement(peer1, 9) == secondAgreement); + delete staleFragments->at(0); + delete staleFragments->at(1); + delete staleFragments; + + auto currentDelba = makeShared(); + currentDelba->setReceiverAddress(peer1); + currentDelba->setTid(9); + currentDelba->setInitiator(true); + auto currentPacket = new Packet("originatorCurrentDelba", currentDelba); + currentPacket->addTag()->setGenerationId(secondGenerationId); + auto terminatedSecondAgreement = expiryHandler.processTransmittedDelba(currentPacket, &replacementCallback); + ASSERT(terminatedSecondAgreement != nullptr); + ASSERT(expiryHandler.getAgreement(peer1, 9) == nullptr); + ASSERT(expiryHandler.processTransmittedDelba(currentPacket, &replacementCallback) == nullptr); + delete currentPacket; +} + +// A recipient inactivity DELBA gets a handler-owned generation on every +// accepted replacement. Stale final fragments cannot remove the replacement, +// while a current-generation final DELBA removes it exactly once. +{ + TestRecipientHandler expiryHandler; + TestRecipientPolicy expiryPolicy; + expiryPolicy.blockAckTimeoutValue = 1; + TestCallback expiryCallback; + auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); + request->setBlockAckTimeoutValue(1); + auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + wait(1); + expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + auto stalePacket = expiryCallback.managementPacket; + auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); + ASSERT(stalePacket != nullptr && staleDelba != nullptr); + ASSERT(stalePacket->getTag()->getGenerationId() == firstGenerationId); + + auto replacementRequest = makeRequest(peer2, 10, 102, SequenceNumberCyclic(311), 32); + replacementRequest->setBlockAckTimeoutValue(1); + auto secondAgreement = expiryHandler.processReceivedAddbaRequest(replacementRequest, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(secondAgreement != nullptr); + auto secondGenerationId = secondAgreement->getGenerationId(); + ASSERT(secondGenerationId != firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(stalePacket, staleDelba)); + ASSERT(expiryHandler.processTransmittedDelba(stalePacket) == nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == secondAgreement); + ASSERT(std::find(expiryCallback.cancelledTeardownGenerationIds.begin(), expiryCallback.cancelledTeardownGenerationIds.end(), firstGenerationId) != expiryCallback.cancelledTeardownGenerationIds.end()); + + auto staleFragmentBody = staticPtrCast(staleDelba->dupShared()); + staleFragmentBody->setSequenceNumber(SequenceNumberCyclic(312)); + auto staleFragmentSource = new Packet("recipientStaleDelbaSource", staleFragmentBody); + staleFragmentSource->addTag()->setGenerationId(firstGenerationId); + staleFragmentSource->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto staleFragments = fragmentation.fragmentFrame(staleFragmentSource, { 4, 2 }); + for (auto fragment : *staleFragments) + ASSERT(fragment->getTag()->getGenerationId() == firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(staleFragments->at(0), findFragmentedActionContext(staleFragments->at(0)))); + ASSERT(expiryHandler.processTransmittedDelba(staleFragments->at(1)) == nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == secondAgreement); + delete staleFragments->at(0); + delete staleFragments->at(1); + delete staleFragments; + + auto currentPacket = makeTaggedRecipientDelbaPacket("recipientCurrentDelba", peer2, 10, SequenceNumberCyclic(313), 0, false, secondGenerationId); + auto terminatedAgreement = expiryHandler.processTransmittedDelba(currentPacket); + ASSERT(terminatedAgreement != nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == nullptr); + ASSERT(expiryHandler.processTransmittedDelba(currentPacket) == nullptr); + delete currentPacket; +} + +// HCF routes recipient DELBAs to the recipient owner and rebuilds its +// eligibility index when replacement or final teardown changes the generation. +// The replacement callback removes queued stale siblings; the active current +// frame is then consumed exactly once. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy hcfOriginatorPolicy; + TestRecipientPolicy hcfRecipientPolicy; + TestCallback managementCallback; + auto request = makeRequest(peer2, 11, 103, SequenceNumberCyclic(320), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(request, &hcfRecipientPolicy, &managementCallback, &managementCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + QosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestRecipientDataService recipientDataService; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &hcfOriginatorPolicy, recipientHandler, &hcfRecipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + hcf.configureManagementCallback(&managementCallback); + hcf.observePendingQueue(&pendingQueue); + + auto stalePacket = makeTaggedRecipientDelbaPacket("hcfStaleRecipientDelba", peer2, 11, SequenceNumberCyclic(321), 0, false, firstGenerationId); + pendingQueue.enqueuePacket(stalePacket); + hcf.trackFrame(stalePacket, AC_BE); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 1); + + auto replacementRequest = makeRequest(peer2, 11, 104, SequenceNumberCyclic(322), 32); + hcf.processReceivedManagementFrame(replacementRequest); + auto secondAgreement = recipientHandler->getAgreement(11, peer2); + ASSERT(secondAgreement != nullptr); + ASSERT(secondAgreement->getGenerationId() != firstGenerationId); + ASSERT(pendingQueue.isEmpty()); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 0); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.numRebuildEligibilityCalls > 0); + + auto currentPacket = makeTaggedRecipientDelbaPacket("hcfCurrentRecipientDelba", peer2, 11, SequenceNumberCyclic(323), 0, false, secondAgreement->getGenerationId()); + pendingQueue.enqueuePacket(currentPacket); + hcf.trackFrame(currentPacket, AC_BE); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 1); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.processTransmittedManagement(currentPacket, currentPacket->peekAtFront(), AC_BE); + ASSERT(recipientHandler->getAgreement(11, peer2) == nullptr); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 2); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 0); + hcf.processTransmittedManagement(currentPacket, currentPacket->peekAtFront(), AC_BE); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 2); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + pendingQueue.removePacket(currentPacket); + delete currentPacket; +} + +// A rejected peer DELBA must not retire a locally pending recipient teardown. +// Once the same DELBA is accepted, HCF cancels exactly that generation without +// repeating the first-transmission reset or deletion notification. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestRecipientDataService recipientDataService; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + auto request = makeRequest(peer2, 14, 109, SequenceNumberCyclic(370), 32); + auto agreement = recipientHandler->processReceivedAddbaRequest(request, &recipientPolicy, &responseCallback, &hcf); + ASSERT(agreement != nullptr); + auto generationId = agreement->getGenerationId(); + auto pendingPacket = makeTaggedRecipientDelbaPacket("recipientRejectedPeerDelba", peer2, 14, SequenceNumberCyclic(371), 0, false, generationId); + auto pendingHeader = pendingPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(pendingPacket); + ackHandler.frameGotInProgress(pendingHeader); + hcf.trackFrame(pendingPacket, AC_BE); + hcf.processTransmittedManagement(pendingPacket, pendingHeader, AC_BE); + ASSERT(recipientHandler->getAgreement(14, peer2) == nullptr); + ASSERT(recipientHandler->isDelbaPending(pendingPacket, pendingHeader)); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + hcf.processFailedFrame(pendingPacket); + + recipientPolicy.delbaAccepted = false; + auto rejectedDelba = makeShared(); + rejectedDelba->setTransmitterAddress(peer2); + rejectedDelba->setReceiverAddress(peer1); + rejectedDelba->setTid(14); + rejectedDelba->setInitiator(true); + hcf.processReceivedManagementFrame(rejectedDelba); + ASSERT(recipientHandler->isDelbaPending(pendingPacket, pendingHeader)); + ASSERT(inProgressFrames.getLength() == 1); + ASSERT(inProgressFrames.getFrameToTransmit() == pendingPacket); + ASSERT(hcf.hasFrame(AC_BE)); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + ASSERT(dropListener.numOtherPacketDrops == 0); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + + recipientPolicy.delbaAccepted = true; + auto acceptedDelba = makeShared(); + acceptedDelba->setTransmitterAddress(peer2); + acceptedDelba->setReceiverAddress(peer1); + acceptedDelba->setTid(14); + acceptedDelba->setInitiator(true); + hcf.processReceivedManagementFrame(acceptedDelba); + ASSERT(recipientHandler->getPendingTeardownGenerationId(14, peer2) == 0); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(!hcf.hasFrame(AC_BE)); + ASSERT(hcf.numCancelledBlockAckTeardowns == 2); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + ASSERT(dropListener.numOtherPacketDrops == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + auto staleProbe = makeTaggedRecipientDelbaPacket("recipientRejectedPeerDelbaProbe", peer2, 14, SequenceNumberCyclic(372), 0, false, generationId); + ASSERT(!recipientHandler->isDelbaPending(staleProbe, staleProbe->peekAtFront())); + delete staleProbe; + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// A tagged originator DELBA removes the agreement on its first transmission, +// but remains selectable through an unacknowledged retry. ACK, terminal abort, +// and replacement each retire only their exact teardown generation. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto firstGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto acknowledgedPacket = makeTaggedDelbaPacket("originatorRetryAcknowledged", peer1, 12, SequenceNumberCyclic(350), 0, false, firstGenerationId); + auto acknowledgedHeader = acknowledgedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(acknowledgedPacket); + ackHandler.frameGotInProgress(acknowledgedHeader); + hcf.trackFrame(acknowledgedPacket, AC_BE); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedHeader, AC_BE); + ASSERT(originatorHandler->getAgreement(peer1, 12) == nullptr); + ASSERT(originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedHeader)); + ASSERT(deletionListener.numSignals == 1); + hcf.processFailedFrame(acknowledgedPacket); + ASSERT(originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == acknowledgedPacket); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedPacket->peekAtFront(), AC_BE); + Packet acknowledgedAck("originatorRetryAck", makeShared()); + hcf.processReceivedAck(&acknowledgedAck, acknowledgedPacket, AC_BE); + ASSERT(!originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 1); + ASSERT(inProgressFrames.getLength() == 0); + auto acknowledgedFrames = inProgressFrames.releaseFrames(); + ASSERT(acknowledgedFrames == std::vector({ acknowledgedPacket })); + delete acknowledgedPacket; + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto abortedGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto abortedPacket = makeTaggedDelbaPacket("originatorRetryAborted", peer1, 12, SequenceNumberCyclic(351), 0, false, abortedGenerationId); + auto abortedHeader = abortedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(abortedPacket); + ackHandler.frameGotInProgress(abortedHeader); + hcf.trackFrame(abortedPacket, AC_BE); + hcf.processTransmittedManagement(abortedPacket, abortedHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(abortedPacket, abortedHeader)); + hcf.processFailedFrame(abortedPacket); + ASSERT(originatorHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == abortedPacket); + hcf.processTransmittedManagement(abortedPacket, abortedPacket->peekAtFront(), AC_BE); + hcf.processFailedFrame(abortedPacket); + ASSERT(!originatorHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 2); + auto abortedFrames = inProgressFrames.releaseFrames(); + ASSERT(abortedFrames == std::vector({ abortedPacket })); + delete abortedPacket; + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto replacedGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto replacedPacket = makeTaggedDelbaPacket("originatorRetryReplaced", peer1, 12, SequenceNumberCyclic(352), 0, false, replacedGenerationId); + auto replacedHeader = replacedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(replacedPacket); + ackHandler.frameGotInProgress(replacedHeader); + hcf.trackFrame(replacedPacket, AC_BE); + hcf.processTransmittedManagement(replacedPacket, replacedHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(replacedPacket, replacedHeader)); + TestCallback replacementCallback; + Packet replacementTrigger("originatorRetryReplacementTrigger"); + auto replacementHeader = makeQosHeader(peer1, 12, SequenceNumberCyclic(353)); + auto obsoleteGenerationId = originatorHandler->processAcknowledgedDataFrame(&replacementTrigger, replacementHeader, &originatorPolicy, &replacementCallback); + ASSERT(obsoleteGenerationId == replacedGenerationId); + hcf.cancelTeardown(true, peer1, 12, obsoleteGenerationId); + auto replacedProbe = makeTaggedDelbaPacket("originatorRetryReplacedProbe", peer1, 12, SequenceNumberCyclic(354), 0, false, replacedGenerationId); + ASSERT(!originatorHandler->isDelbaPending(replacedProbe, replacedProbe->peekAtFront())); + delete replacedProbe; + ASSERT(originatorHandler->getAgreement(peer1, 12) != nullptr); + ASSERT(originatorHandler->getAgreement(peer1, 12)->getTransactionId() != replacedGenerationId); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(deletionListener.numSignals == 3); + ASSERT(dropListener.numOtherPacketDrops == 1); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// Recipient teardown generations follow the same retry lifetime as +// originator generations, including recipient-side ACK/abort callbacks and +// replacement disposal while an old final DELBA is still in progress. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestRecipientDataService recipientDataService; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + auto firstRequest = makeRequest(peer2, 13, 105, SequenceNumberCyclic(360), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(firstRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + auto acknowledgedPacket = makeTaggedRecipientDelbaPacket("recipientRetryAcknowledged", peer2, 13, SequenceNumberCyclic(361), 0, false, firstGenerationId); + auto acknowledgedHeader = acknowledgedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(acknowledgedPacket); + ackHandler.frameGotInProgress(acknowledgedHeader); + hcf.trackFrame(acknowledgedPacket, AC_BE); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedHeader, AC_BE); + ASSERT(recipientHandler->getAgreement(13, peer2) == nullptr); + ASSERT(recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedHeader)); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + hcf.processFailedFrame(acknowledgedPacket); + ASSERT(recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == acknowledgedPacket); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedPacket->peekAtFront(), AC_BE); + Packet acknowledgedAck("recipientRetryAck", makeShared()); + hcf.processReceivedAck(&acknowledgedAck, acknowledgedPacket, AC_BE); + ASSERT(!recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + auto acknowledgedFrames = inProgressFrames.releaseFrames(); + ASSERT(acknowledgedFrames == std::vector({ acknowledgedPacket })); + delete acknowledgedPacket; + + auto abortedRequest = makeRequest(peer2, 13, 106, SequenceNumberCyclic(362), 32); + auto abortedAgreement = recipientHandler->processReceivedAddbaRequest(abortedRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(abortedAgreement != nullptr); + auto abortedGenerationId = abortedAgreement->getGenerationId(); + auto abortedPacket = makeTaggedRecipientDelbaPacket("recipientRetryAborted", peer2, 13, SequenceNumberCyclic(363), 0, false, abortedGenerationId); + auto abortedHeader = abortedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(abortedPacket); + ackHandler.frameGotInProgress(abortedHeader); + hcf.trackFrame(abortedPacket, AC_BE); + hcf.processTransmittedManagement(abortedPacket, abortedHeader, AC_BE); + ASSERT(recipientHandler->isDelbaPending(abortedPacket, abortedHeader)); + hcf.processFailedFrame(abortedPacket); + ASSERT(recipientHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == abortedPacket); + hcf.processTransmittedManagement(abortedPacket, abortedPacket->peekAtFront(), AC_BE); + hcf.processFailedFrame(abortedPacket); + ASSERT(!recipientHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 2); + ASSERT(recipientDataService.numReorderingResets == 2); + auto abortedFrames = inProgressFrames.releaseFrames(); + ASSERT(abortedFrames == std::vector({ abortedPacket })); + delete abortedPacket; + + auto replacedRequest = makeRequest(peer2, 13, 107, SequenceNumberCyclic(364), 32); + auto replacedAgreement = recipientHandler->processReceivedAddbaRequest(replacedRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(replacedAgreement != nullptr); + auto replacedGenerationId = replacedAgreement->getGenerationId(); + auto replacedPacket = makeTaggedRecipientDelbaPacket("recipientRetryReplaced", peer2, 13, SequenceNumberCyclic(365), 0, false, replacedGenerationId); + auto replacedHeader = replacedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(replacedPacket); + ackHandler.frameGotInProgress(replacedHeader); + hcf.trackFrame(replacedPacket, AC_BE); + hcf.processTransmittedManagement(replacedPacket, replacedHeader, AC_BE); + ASSERT(recipientHandler->isDelbaPending(replacedPacket, replacedHeader)); + auto replacementRequest = makeRequest(peer2, 13, 108, SequenceNumberCyclic(366), 32); + auto replacementAgreement = recipientHandler->processReceivedAddbaRequest(replacementRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(replacementAgreement != nullptr); + ASSERT(replacementAgreement->getGenerationId() != replacedGenerationId); + auto replacedProbe = makeTaggedRecipientDelbaPacket("recipientRetryReplacedProbe", peer2, 13, SequenceNumberCyclic(367), 0, false, replacedGenerationId); + ASSERT(!recipientHandler->isDelbaPending(replacedProbe, replacedProbe->peekAtFront())); + delete replacedProbe; + ASSERT(recipientHandler->getAgreement(13, peer2) == replacementAgreement); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(deletionListener.numSignals == 3); + ASSERT(recipientDataService.numReorderingResets == 3); + ASSERT(dropListener.numOtherPacketDrops == 1); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From 683b8ea93f244484366e32fcdd4d2b3c5c18da15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:44:39 +0200 Subject: [PATCH 34/91] ieee80211: add+fix: use absolute Block Ack inactivity deadlines The shared HCF inactivity timer must represent the earliest enabled agreement deadline and cancel when no agreement remains. Terminal queue callbacks can remove current or sibling agreements during expiry, so snapshot peer/TID/generation values and relookup them before each action. Direct mutation tests and real bounded-queue HCF expiry cover both roles. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.add+change.fix | test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 7 + doc/src/migration-guide/index.rst | 10 + .../blockack/OriginatorBlockAckAgreement.h | 10 +- .../OriginatorBlockAckAgreementHandler.cc | 68 ++++-- .../OriginatorBlockAckAgreementHandler.h | 2 +- .../mac/blockack/RecipientBlockAckAgreement.h | 10 +- .../RecipientBlockAckAgreementHandler.cc | 98 +++++--- .../RecipientBlockAckAgreementHandler.h | 7 +- .../IBlockAckAgreementHandlerCallback.h | 10 +- .../IOriginatorBlockAckAgreementHandler.h | 10 +- .../IRecipientBlockAckAgreementHandler.h | 15 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 41 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 7 +- .../Ieee80211BlockAckInactivityTimer_1.test | 129 +++++++++++ .../Ieee80211HcfAgreementQueueDrop_1.test | 169 ++++++++++++++ tests/unit/Ieee80211AddbaTransaction_1.test | 213 +++++++++++++++++- .../Ieee80211OriginatorExpiryReentrant_1.test | 91 ++++++++ .../Ieee80211RecipientExpiryReentrant_1.test | 89 ++++++++ 18 files changed, 917 insertions(+), 69 deletions(-) create mode 100644 tests/module/Ieee80211BlockAckInactivityTimer_1.test create mode 100644 tests/module/Ieee80211HcfAgreementQueueDrop_1.test create mode 100644 tests/unit/Ieee80211OriginatorExpiryReentrant_1.test create mode 100644 tests/unit/Ieee80211RecipientExpiryReentrant_1.test diff --git a/WHATSNEW b/WHATSNEW index 2d1373b2091..a21d2239609 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -393,6 +393,13 @@ Notable backward incompatible changes are the following: instead of completing an old body; affected application deliveries, forwarding trajectories and fingerprints change. +8. Block Ack Inactivity Timer Callbacks + + Block Ack inactivity uses absolute per-agreement deadlines. The shared timer follows + the earliest enabled deadline and is cancelled when none remains. Agreement expiry is + safe when a bounded management queue drops a generated DELBA synchronously. Timers + and traffic may differ from runs that kept extending an inactivity 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 581e4b62efb..1d1bfac9ff2 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -156,6 +156,16 @@ generation metadata remains available. Keep teardown pending until its final fra acknowledged or the exchange terminates; do not retire a replacement generation from a stale completion. +Block Ack Inactivity Timer Callbacks +------------------------------------ + +Schedule the shared Block Ack inactivity timer for the earliest enabled absolute +agreement deadline and cancel it when no enabled deadline remains. Expiry callbacks can +synchronously remove or replace the current agreement or a sibling. Keep +peer/TID/generation values as the work list and relookup the live state before using it +after a callback; incrementing a map iterator before calling out does not protect a +removed sibling. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index 4533d0c18fa..d4d1644a16b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -38,6 +38,9 @@ class INET_API OriginatorBlockAckAgreement : public cObject simtime_t addbaResponseDeadline = -1; simtime_t blockAckTimeoutValue = -1; simtime_t expirationTime = -1; + // The agreement stays installed until the timeout DELBA is transmitted; + // prevent that pending teardown from being re-armed by late activity. + bool inactivityExpired = false; public: OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported, uint8_t dialogToken, uint64_t transactionId) : @@ -79,8 +82,13 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } virtual void baPolicyFrameSent() { numSentBaPolicyFrames++; } - virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual void calculateExpirationTime() { + if (!inactivityExpired) + expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; + } virtual simtime_t getExpirationTime() { return expirationTime; } + virtual bool isInactivityExpired() const { return inactivityExpired; } + virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2b1e4bbbd8a..9245bf55fe7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -38,7 +38,7 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() simtime_t earliestTime = SIMTIME_MAX; for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getIsAddbaResponseReceived()) { + if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired()) { ASSERT(earliestTime >= 0); ASSERT(agreement->getExpirationTime() >= 0); earliestTime = std::min(earliestTime, agreement->getExpirationTime()); @@ -103,18 +103,29 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. - // The procedure is illustrated in Figure 10-14. + // The procedure is illustrated in IEEE 802.11-2024, Figure 11-34. simtime_t now = simTime(); - for (auto id : blockAckAgreements) { - auto agreement = id.second; - if (agreement->getExpirationTime() == now) { - MacAddress receiverAddr = id.first.first; - Tid tid = id.first.second; - const auto& delba = buildDelba(receiverAddr, tid, 39); - auto delbaPacket = new Packet("Delba", delba); - delbaPacket->addTag()->setGenerationId(agreement->getTransactionId()); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + // Queue callbacks can erase the current agreement, a sibling, or replace + // either generation. Keep only value identities across outward calls. + std::vector, uint64_t>> expiredAgreements; + for (const auto& entry : blockAckAgreements) { + auto agreement = entry.second; + if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) + expiredAgreements.emplace_back(entry.first, agreement->getTransactionId()); + } + for (const auto& entry : expiredAgreements) { + auto receiverAddr = entry.first.first; + auto tid = entry.first.second; + auto generationId = entry.second; + auto agreement = getAgreement(receiverAddr, tid); + if (agreement == nullptr || agreement->getTransactionId() != generationId || + !agreement->getIsAddbaResponseReceived() || agreement->isInactivityExpired() || agreement->getExpirationTime() > now) + continue; + agreement->markInactivityExpired(); + const auto& delba = buildDelba(receiverAddr, tid, 39); + auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(generationId); + procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT } scheduleInactivityTimer(agreementHandlerCallback); } @@ -144,7 +155,7 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); - if (agreement) { + if (agreement && !agreement->isInactivityExpired()) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -157,8 +168,8 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::ORIGINATOR, earliestExpirationTime); } void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) @@ -362,6 +373,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: return nullptr; bool cancelPendingTransaction = agreement->isPending(); std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) @@ -373,6 +385,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); @@ -396,22 +409,34 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet return true; } -bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) - return false; + return {}; auto agreementTag = packet->findTag(); if (agreementTag != nullptr) { - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownTransactionIds.find(agreementId); if (it != pendingTeardownTransactionIds.end() && it->second == agreementTag->getGenerationId()) { auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); - return true; + if (callback != nullptr) + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); + return { true, nullptr }; + } + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + if (agreement != nullptr && agreement->getTransactionId() == agreementTag->getGenerationId() && agreement->isInactivityExpired()) { + OriginatorBlockAckAgreementAbortResult result; + result.handled = true; + result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + if (callback != nullptr) + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); + return result; } } - return false; + return {}; } std::unique_ptr OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -426,6 +451,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: if (pendingTeardownIt != pendingTeardownTransactionIds.end()) pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 0eb290334c6..891c01432c9 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -53,7 +53,7 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 6bb5da30c25..4002ef76b2a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -22,6 +22,9 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; simtime_t expirationTime = -1; + // The agreement stays installed until the timeout DELBA is transmitted; + // prevent that pending teardown from being re-armed by late activity. + bool inactivityExpired = false; uint64_t generationId = 0; public: @@ -36,8 +39,13 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } virtual uint64_t getGenerationId() const { return generationId; } - virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual void calculateExpirationTime() { + if (!inactivityExpired) + expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; + } virtual simtime_t getExpirationTime() { return expirationTime; } + virtual bool isInactivityExpired() const { return inactivityExpired; } + virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } friend std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement); }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 9c6d41c29f8..4516318e9b3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -19,7 +19,8 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() simtime_t earliestTime = SIMTIME_MAX; for (auto id : blockAckAgreements) { auto agreement = id.second; - earliestTime = std::min(earliestTime, agreement->getExpirationTime()); + if (!agreement->isInactivityExpired()) + earliestTime = std::min(earliestTime, agreement->getExpirationTime()); } return earliestTime; } @@ -27,8 +28,8 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback) { simtime_t earliestExpirationTime = computeEarliestExpirationTime(); - if (earliestExpirationTime != SIMTIME_MAX) - callback->scheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, earliestExpirationTime); } // The inactivity timer at a recipient is reset when MPDUs corresponding to the TID for which the Block Ack @@ -41,8 +42,21 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const PtrgetTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); auto agreement = getAgreement(tid, originatorAddr); - if (agreement) + if (agreement && !agreement->isInactivityExpired()) { + agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); + } + } +} + +// IEEE Std 802.11-2024, 11.5.4: a Basic BlockAckReq for an agreement's TID +// also resets the recipient inactivity timer. +void RecipientBlockAckAgreementHandler::blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) +{ + auto agreement = getAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + if (agreement != nullptr && !agreement->isInactivityExpired()) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); } } @@ -51,18 +65,29 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. - // The procedure is illustrated in Figure 10-14. + // The procedure is illustrated in IEEE 802.11-2024, Figure 11-34. simtime_t now = simTime(); - for (auto id : blockAckAgreements) { - auto agreement = id.second; - if (agreement->getExpirationTime() == now) { - MacAddress receiverAddr = id.first.first; - Tid tid = id.first.second; - const auto& delba = buildDelba(receiverAddr, tid, 39); - auto delbaPacket = new Packet("Delba", delba); - delbaPacket->addTag()->setGenerationId(agreement->getGenerationId()); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + // Queue callbacks can erase the current agreement, a sibling, or replace + // either generation. Keep only value identities across outward calls. + std::vector, uint64_t>> expiredAgreements; + for (const auto& entry : blockAckAgreements) { + auto agreement = entry.second; + if (!agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) + expiredAgreements.emplace_back(entry.first, agreement->getGenerationId()); + } + for (const auto& entry : expiredAgreements) { + auto receiverAddr = entry.first.first; + auto tid = entry.first.second; + auto generationId = entry.second; + auto agreement = getAgreement(tid, receiverAddr); + if (agreement == nullptr || agreement->getGenerationId() != generationId || + agreement->isInactivityExpired() || agreement->getExpirationTime() > now) + continue; + agreement->markInactivityExpired(); + const auto& delba = buildDelba(receiverAddr, tid, 39); + auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(generationId); + procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT } scheduleInactivityTimer(agreementHandlerCallback); } @@ -70,7 +95,7 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb // // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the peer STA with the Reason Code // field set to TIMEOUT and shall issue a MLME-DELBA.indication primitive with the ReasonCode -// parameter having a value of TIMEOUT. The procedure is illustrated in Figure 10-14. +// parameter having a value of TIMEOUT. The procedure is illustrated in IEEE 802.11-2024, Figure 11-34. // const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode) { @@ -203,7 +228,7 @@ bool RecipientBlockAckAgreementHandler::isDelbaPending(const Packet *packet, con return teardownIt != pendingTeardownGenerationIds.end() && teardownIt->second == agreementTag->getGenerationId(); } -std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet) +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (delba->getInitiator()) @@ -220,6 +245,7 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p if (agreement->getGenerationId() != agreementTag->getGenerationId()) return nullptr; auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); return terminatedAgreement; } @@ -228,7 +254,9 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p return nullptr; return nullptr; } - return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + return terminatedAgreement; } bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) @@ -250,23 +278,34 @@ bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, return true; } -bool RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreementAbortResult RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (delba->getInitiator()) - return false; + return {}; auto agreementTag = packet->findTag(); if (agreementTag == nullptr) - return false; + return {}; auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); auto it = pendingTeardownGenerationIds.find(agreementId); - if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) - return false; - auto generationId = it->second; - pendingTeardownGenerationIds.erase(it); - if (callback != nullptr) - callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); - return true; + if (it != pendingTeardownGenerationIds.end() && it->second == agreementTag->getGenerationId()) { + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return { true, nullptr }; + } + auto agreement = getAgreement(delba->getTid(), delba->getReceiverAddress()); + if (agreement != nullptr && agreement->getGenerationId() == agreementTag->getGenerationId() && agreement->isInactivityExpired()) { + RecipientBlockAckAgreementAbortResult result; + result.handled = true; + result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); + return result; + } + return {}; } uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const @@ -275,7 +314,7 @@ uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid t return it == pendingTeardownGenerationIds.end() ? 0 : it->second; } -std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); @@ -283,6 +322,7 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p if (pendingTeardownIt != pendingTeardownGenerationIds.end()) pendingTeardownGenerationIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleInactivityTimer(callback); return terminatedAgreement; } return nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 3188e24b6d6..4d979ef547e 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -44,11 +44,12 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual ~RecipientBlockAckAgreementHandler(); virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; - virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual std::unique_ptr processTransmittedDelba(Packet *packet) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index 77c842be775..7de2599b89f 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -18,12 +18,20 @@ class Packet; namespace ieee80211 { +enum class BlockAckAgreementRole +{ + ORIGINATOR, + RECIPIENT, +}; + class INET_API IBlockAckAgreementHandlerCallback { public: virtual ~IBlockAckAgreementHandlerCallback() {} - virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + // The deadline is an absolute simulation timestamp. The role identifies + // which half of the shared HCF inactivity timer supplied the deadline. + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; // Removes queued siblings of a sender-local DELBA without assuming that diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 068c90f7d35..02000861822 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -32,6 +32,14 @@ struct INET_API OriginatorBlockAckAgreementResponse uint64_t teardownTransactionId = 0; }; +struct INET_API OriginatorBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + class INET_API IOriginatorBlockAckAgreementHandler { public: @@ -49,7 +57,7 @@ class INET_API IOriginatorBlockAckAgreementHandler // Returns true when the packet completed or aborted its tagged teardown // transaction and sibling packets were cancelled through the callback. virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index ded64d55b0a..d853ece3f21 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -24,6 +24,14 @@ class Packet; namespace ieee80211 { +struct INET_API RecipientBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + class INET_API IRecipientBlockAckAgreementHandler { public: @@ -31,11 +39,12 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; - virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual std::unique_ptr processTransmittedDelba(Packet *packet) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; - virtual bool processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 6630a4b5b15..8d19b3fb81c 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -9,6 +9,8 @@ #include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -158,7 +160,24 @@ bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) auto agreementTag = packet->findTag(); if (agreementTag != nullptr && blockAckTeardownsBeingCancelled.find(std::make_tuple(delba->getInitiator(), delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId())) != blockAckTeardownsBeingCancelled.end()) return false; - bool aborted = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + bool aborted = false; + if (delba->getInitiator()) { + if (originatorBlockAckAgreementHandler != nullptr) { + auto result = originatorBlockAckAgreementHandler->processAbortedDelba(packet, this); + aborted = result.handled; + if (result.terminatedAgreement != nullptr && result.terminatedAgreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, result.terminatedAgreement.get()); + } + } + else if (recipientBlockAckAgreementHandler != nullptr) { + auto result = recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + aborted = result.handled; + if (result.terminatedAgreement != nullptr) { + if (recipientDataService != nullptr) + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); + emit(blockAckAgreementDeletedSignal, result.terminatedAgreement.get()); + } + } if (aborted) { rebuildPendingFrameEligibility(); return true; @@ -431,10 +450,20 @@ void Hcf::scheduleStartRxTimer(simtime_t timeout) scheduleAfter(timeout, startRxTimer); } -void Hcf::scheduleInactivityTimer(simtime_t timeout) +void Hcf::scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) { Enter_Method("scheduleInactivityTimer"); - rescheduleAfter(timeout, inactivityTimer); + if (role == BlockAckAgreementRole::ORIGINATOR) + originatorInactivityDeadline = deadline; + else + recipientInactivityDeadline = deadline; + auto earliestDeadline = std::min(originatorInactivityDeadline, recipientInactivityDeadline); + if (earliestDeadline == SIMTIME_MAX) { + if (inactivityTimer->isScheduled()) + cancelEvent(inactivityTimer); + } + else + rescheduleAt(std::max(earliestDeadline, simTime()), inactivityTimer); } void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) @@ -748,6 +777,8 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); else if (auto blockAckRequest = dynamicPtrCast(header)) { + if (recipientBlockAckAgreementHandler) + recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckRequest, this); if (recipientBlockAckProcedure) recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckRequest, recipientAckPolicy, recipientBlockAckAgreementHandler, this); } @@ -806,7 +837,7 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrgetInitiator()) { auto pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); - auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy, this); auto remainingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); if (agreement != nullptr) { recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); @@ -989,7 +1020,7 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< } } else { - auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet); + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet, this); if (agreement != nullptr) { // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient // resources are released whether the recipient transmitted or diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index ae3c2a9ab9f..5f0ab335097 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -65,6 +65,11 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; cMessage *addbaResponseTimer = nullptr; + // The two agreement handlers share one timer but publish independent + // absolute deadlines. Keep both until the handlers explicitly retire + // their role so one role cannot cancel the other's timeout. + simtime_t originatorInactivityDeadline = SIMTIME_MAX; + simtime_t recipientInactivityDeadline = SIMTIME_MAX; // Transmission and Reception IRx *rx = nullptr; @@ -196,7 +201,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; // IProcedureCallback - virtual void scheduleInactivityTimer(simtime_t timeout) override; + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; diff --git a/tests/module/Ieee80211BlockAckInactivityTimer_1.test b/tests/module/Ieee80211BlockAckInactivityTimer_1.test new file mode 100644 index 00000000000..bdd20495297 --- /dev/null +++ b/tests/module/Ieee80211BlockAckInactivityTimer_1.test @@ -0,0 +1,129 @@ +%description: +Exercise the shared HCF Block Ack inactivity timer with independent originator +and recipient deadlines. Verify that the timer keeps the earliest absolute +deadline, preserves the other role when one role is cleared, and is cancelled +only after both roles publish SIMTIME_MAX. + +%file: TestIeee80211BlockAckInactivityTimer.cc + +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" + +namespace inet { +namespace ieee80211 { + +class TestHcfInactivityTimer : public Hcf +{ + public: + void updateInactivityDeadline(BlockAckAgreementRole role, simtime_t deadline) { scheduleInactivityTimer(role, deadline); } + simtime_t getOriginatorDeadline() const { return originatorInactivityDeadline; } + simtime_t getRecipientDeadline() const { return recipientInactivityDeadline; } + bool isInactivityTimerScheduled() const { return inactivityTimer->isScheduled(); } + simtime_t getInactivityTimerArrivalTime() const { return inactivityTimer->getArrivalTime(); } + + protected: + virtual int numInitStages() const override { return 1; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) + inactivityTimer = new cMessage("blockAckInactivityTimer"); + } +}; + +Define_Module(TestHcfInactivityTimer); + +class Ieee80211BlockAckInactivityTimerTest : public cSimpleModule +{ + protected: + TestHcfInactivityTimer *hcf = nullptr; + + protected: + virtual void initialize() override + { + hcf = check_and_cast(getModuleByPath("^.hcf")); + scheduleAt(SimTime(1, SIMTIME_US), new cMessage("checkInactivityTimer")); + } + + virtual void handleMessage(cMessage *message) override + { + auto startTime = simTime(); + ASSERT(startTime > 0); + + auto originatorDeadline = startTime + SimTime(2, SIMTIME_US); + auto recipientDeadline = startTime + SimTime(5, SIMTIME_US); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, originatorDeadline); + ASSERT(hcf->getOriginatorDeadline() == originatorDeadline); + ASSERT(hcf->getRecipientDeadline() == SIMTIME_MAX); + ASSERT(hcf->isInactivityTimerScheduled()); + ASSERT(hcf->getInactivityTimerArrivalTime() == originatorDeadline); + + // A later recipient deadline must not replace the earlier originator + // deadline in the shared timer. + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, recipientDeadline); + ASSERT(hcf->getRecipientDeadline() == recipientDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == originatorDeadline); + + // Clearing one role leaves the other role's deadline armed. + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, SIMTIME_MAX); + ASSERT(hcf->getOriginatorDeadline() == SIMTIME_MAX); + ASSERT(hcf->getInactivityTimerArrivalTime() == recipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(hcf->getRecipientDeadline() == SIMTIME_MAX); + ASSERT(!hcf->isInactivityTimerScheduled()); + + // The reverse update order still replaces the shared timer with an + // earlier deadline and preserves the later role when it is cleared. + auto reverseRecipientDeadline = startTime + SimTime(8, SIMTIME_US); + auto reverseOriginatorDeadline = startTime + SimTime(1, SIMTIME_US); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, reverseRecipientDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseRecipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, reverseOriginatorDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseOriginatorDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, SIMTIME_MAX); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseRecipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(!hcf->isInactivityTimerScheduled()); + + delete message; + endSimulation(); + } +}; + +Define_Module(Ieee80211BlockAckInactivityTimerTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; + +simple TestHcfInactivityTimer +{ + parameters: + @class(::inet::ieee80211::TestHcfInactivityTimer); +} + +simple Ieee80211BlockAckInactivityTimerTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211BlockAckInactivityTimerTest); +} + +network Ieee80211BlockAckInactivityTimerTestNetwork +{ + submodules: + hcf: TestHcfInactivityTimer; + test: Ieee80211BlockAckInactivityTimerTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211BlockAckInactivityTimerTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 10us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false diff --git a/tests/module/Ieee80211HcfAgreementQueueDrop_1.test b/tests/module/Ieee80211HcfAgreementQueueDrop_1.test new file mode 100644 index 00000000000..f2c63eb4ba3 --- /dev/null +++ b/tests/module/Ieee80211HcfAgreementQueueDrop_1.test @@ -0,0 +1,169 @@ +%description: +Exercise real HCF terminal cleanup when bounded management queues drop expiry +DELBAs for both roles and when RED rejects a pending ADDBA before enqueue. + +%file: TestHcfAgreementQueueDrop.cc +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + +namespace inet { +namespace ieee80211 { + +class SeedOriginatorHandler : public OriginatorBlockAckAgreementHandler +{ + public: + void seed(MacAddress peer, Tid tid) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(0), 64, true, false, 1, nextTransactionId++); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SimTime(1, SIMTIME_US)); + agreement->calculateExpirationTime(); + blockAckAgreements[std::make_pair(peer, tid)] = agreement; + } + +}; + +class SeedRecipientHandler : public RecipientBlockAckAgreementHandler +{ + public: + void seed(MacAddress peer, Tid tid) { + auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(0), 64, SimTime(1, SIMTIME_US), nextAgreementGenerationId++); + agreement->calculateExpirationTime(); + blockAckAgreements[std::make_pair(peer, tid)] = agreement; + } +}; + +class QueueDropHcf : public Hcf +{ + public: + void prepare(MacAddress peer) { + Enter_Method("prepare"); + ASSERT(originatorBlockAckAgreementHandler != nullptr); + delete originatorBlockAckAgreementHandler; + delete recipientBlockAckAgreementHandler; + auto originator = new SeedOriginatorHandler(); + auto recipient = new SeedRecipientHandler(); + originatorBlockAckAgreementHandler = originator; + recipientBlockAckAgreementHandler = recipient; + if (!par("redTest").boolValue()) { + for (int tid = 0; tid < 2; tid++) { + originator->seed(peer, tid); + recipient->seed(peer, tid); + } + scheduleInactivityTimer(BlockAckAgreementRole::ORIGINATOR, simTime() + SimTime(1, SIMTIME_US)); + scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, simTime() + SimTime(1, SIMTIME_US)); + } + } + void checkExpired(MacAddress peer) { + Enter_Method("checkExpired"); + for (int tid = 0; tid < 2; tid++) { + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, tid) == nullptr); + ASSERT(recipientBlockAckAgreementHandler->getAgreement(tid, peer) == nullptr); + } + ASSERT(!inactivityTimer->isScheduled()); + ASSERT(pendingFrameEligibility.empty()); + } +}; +Define_Module(QueueDropHcf); + +class HcfAgreementQueueDropTest : public cSimpleModule, public cListener +{ + public: + HcfAgreementQueueDropTest() : cSimpleModule(65536) {} + protected: + int departures = 0; + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *, cObject *details) override { + if (std::string(source->getName()) == "pendingQueue") { + ASSERT(check_and_cast(details)->getReason() == queueing::IPacketQueue::PacketRemovalReason::DROPPED); + departures++; + } + } + virtual void activity() override { + const MacAddress peer("02:00:00:00:00:09"); + auto hcf = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf")); + auto queueModule = getModuleByPath("^.ap.wlan[0].mac.hcf.edca.edcaf[3].pendingQueue"); + queueModule->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + hcf->prepare(peer); + wait(SimTime(2, SIMTIME_US)); + hcf->checkExpired(peer); + ASSERT(departures == 4); + queueModule->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + wait(SimTime(1, SIMTIME_US)); + std::cout << "HCF agreement queue cleanup verified.\n"; + } +}; +Define_Module(HcfAgreementQueueDropTest); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.queueing.classifier.PacketClassifier; +import inet.queueing.queue.CompoundPacketQueueBase; +import inet.queueing.queue.PacketQueue; +import inet.queueing.scheduler.PriorityScheduler; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module QueueDropHcf extends Hcf { + parameters: + bool redTest = default(false); + @class(::inet::ieee80211::QueueDropHcf); +} +module AgreementPendingQueue extends CompoundPacketQueueBase { + parameters: + @class(::inet::queueing::CompoundPacketQueueBase); + submodules: + classifier: PacketClassifier { classifierClass = "inet::ieee80211::MgmtOverDataClassifier"; } + managementQueue: like inet.queueing.contract.IPacketQueue; + multicastQueue: PacketQueue; + unicastQueue: PacketQueue; + prioritizer: PriorityScheduler; + connections: + in --> classifier.in; + classifier.out++ --> managementQueue.in; + classifier.out++ --> multicastQueue.in; + classifier.out++ --> unicastQueue.in; + managementQueue.out --> prioritizer.in++; + multicastQueue.out --> prioritizer.in++; + unicastQueue.out --> prioritizer.in++; + prioritizer.out --> out; +} +simple HcfAgreementQueueDropTest extends SimpleModule { + parameters: + @class(::inet::ieee80211::HcfAgreementQueueDropTest); +} +network HcfAgreementQueueDropNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + test: HcfAgreementQueueDropTest; +} + +%inifile: omnetpp.ini +[General] +network = HcfAgreementQueueDropNetwork +sim-time-limit = 4us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.ap.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.ap.wlan[0].mac.qosStation = true +*.ap.wlan[0].mac.hcf.isBlockAckSupported = true +*.ap.wlan[0].mac.hcf.typename = "QueueDropHcf" +*.ap.wlan[0].mac.hcf.edca.edcaf[*].pendingQueue.typename = "AgreementPendingQueue" +*.ap.wlan[0].mac.hcf.edca.edcaf[*].pendingQueue.managementQueue.packetCapacity = 0 +*.ap.wlan[0].mac.hcf.edca.edcaf[*].pendingQueue.managementQueue.dropperClass = "inet::queueing::PacketAtCollectionEndDropper" +*.ap.wlan[0].mgmt.beaconInterval = 10s +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.ap.wlan[0].address = "02:00:00:00:00:01" + +%contains: stdout +HCF agreement queue cleanup verified. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index dbd732ff9ff..8725e60ec75 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -106,6 +106,9 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler std::vector managementPackets; Ptr managementHeader; simtime_t addbaDeadline = SIMTIME_MAX; + simtime_t originatorInactivityDeadline = SIMTIME_MAX; + simtime_t recipientInactivityDeadline = SIMTIME_MAX; + std::vector> inactivityDeadlineUpdates; std::vector cancelledTransactionIds; std::vector excludedPackets; std::vector cancelledTeardownGenerationIds; @@ -120,7 +123,13 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler managementPackets.push_back(packet); managementHeader = header; } - virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override { + inactivityDeadlineUpdates.emplace_back(role, deadline); + if (role == BlockAckAgreementRole::ORIGINATOR) + originatorInactivityDeadline = deadline; + else + recipientInactivityDeadline = deadline; + } virtual void scheduleAddbaResponseTimer(simtime_t deadline) override { addbaDeadline = deadline; } virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { cancelledTransactionIds.push_back(transactionId); @@ -140,6 +149,8 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, nextTransactionId++); agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(0); + agreement->calculateExpirationTime(); blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; } void setNextDialogToken(uint8_t token) { nextDialogToken = token; } @@ -147,6 +158,7 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void allowRetryNow(MacAddress receiverAddress, Tid tid) { addbaRetryDeadlines[std::make_pair(receiverAddress, tid)] = simTime(); } void setPendingTeardownTransaction(MacAddress receiverAddress, Tid tid, uint64_t transactionId) { pendingTeardownTransactionIds[std::make_pair(receiverAddress, tid)] = transactionId; } simtime_t getRetryDeadline(MacAddress receiverAddress, Tid tid) const { auto it = addbaRetryDeadlines.find(std::make_pair(receiverAddress, tid)); return it == addbaRetryDeadlines.end() ? SIMTIME_MAX : it->second; } + void publishInactivityDeadline(IBlockAckAgreementHandlerCallback *callback) { scheduleInactivityTimer(callback); } }; class TestTimeoutCancellationCallback : public TestCallback @@ -647,6 +659,7 @@ class TestHcf : public Hcf bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void cancelTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket = nullptr) { cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } + bool processDroppedTeardown(Packet *packet) { return processDroppedBlockAckTeardownFrame(packet); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } @@ -673,7 +686,7 @@ class TestHcf : public Hcf virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override { numCancelledBlockAckTeardowns++; cancelledBlockAckTeardownGenerationIds.push_back(generationId); Hcf::cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } - virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleInactivityTimer(BlockAckAgreementRole, simtime_t) override {} virtual void scheduleAddbaResponseTimer(simtime_t) override {} }; @@ -4919,6 +4932,168 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { delete currentPacket; } +// Inactivity deadlines are absolute simulation times, and expiry is +// one-shot while the tagged DELBA waits in the local transmission queues. +// IEEE Std 802.11-2024, 11.5.4 and 10.25.4. +{ + wait(1); + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 3; + TestCallback setupCallback; + auto request = makeRequest(peer2, 12, 109, SequenceNumberCyclic(330), 32); + request->setBlockAckTimeoutValue(3); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); + ASSERT(agreement != nullptr); + auto creationTime = simTime(); + ASSERT(creationTime > 0); + ASSERT(agreement->getExpirationTime() == creationTime + 3); + ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 3); + + auto basicBlockAckReq = makeShared(); + basicBlockAckReq->setTransmitterAddress(peer2); + basicBlockAckReq->setTidInfo(12); + wait(1); + auto barReceptionTime = simTime(); + recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 3); + ASSERT(agreement->getExpirationTime() > creationTime + 3); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 3); + + wait(4); + TestCallback expiryCallback; + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(recipientHandler.getAgreement(12, peer2) == agreement); + ASSERT(agreement->isInactivityExpired()); + ASSERT(expiryCallback.managementPackets.size() == 1); + ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); + + // The same overdue agreement is not emitted a second time, and a late + // data frame cannot re-arm its inactivity deadline. + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(expiryCallback.managementPackets.size() == 1); + auto lateQosHeader = makeQosHeader(peer2, 12, SequenceNumberCyclic(331)); + lateQosHeader->setTransmitterAddress(peer2); + lateQosHeader->setAckPolicy(AckPolicy::BLOCK_ACK); + recipientHandler.qosFrameReceived(lateQosHeader, &expiryCallback); + ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); + TestCallback lateBarCallback; + recipientHandler.blockAckReqReceived(basicBlockAckReq, &lateBarCallback); + ASSERT(lateBarCallback.recipientInactivityDeadline == SIMTIME_MAX); + + // The originator uses the same absolute-deadline and one-shot rules. + TestOriginatorHandler originatorHandler; + originatorHandler.addEstablishedAgreement(peer1, 13); + auto originatorAgreement = originatorHandler.getAgreement(peer1, 13); + originatorAgreement->setBlockAckTimeoutValue(2); + originatorAgreement->calculateExpirationTime(); + auto originatorCreationTime = simTime(); + TestCallback originatorCallback; + originatorHandler.publishInactivityDeadline(&originatorCallback); + ASSERT(originatorCallback.originatorInactivityDeadline == originatorCreationTime + 2); + wait(3); + originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); + ASSERT(originatorHandler.getAgreement(peer1, 13) == originatorAgreement); + ASSERT(originatorAgreement->isInactivityExpired()); + ASSERT(originatorCallback.managementPackets.size() == 1); + ASSERT(originatorCallback.originatorInactivityDeadline == SIMTIME_MAX); + originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); + ASSERT(originatorCallback.managementPackets.size() == 1); + + // A role with no finite deadline publishes SIMTIME_MAX independently; + // Hcf can therefore retain a finite deadline from the other role. + TestOriginatorHandler noDeadlineOriginatorHandler; + noDeadlineOriginatorHandler.addEstablishedAgreement(peer1, 14); + TestCallback roleCallback; + roleCallback.scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, simTime() + 10); + noDeadlineOriginatorHandler.publishInactivityDeadline(&roleCallback); + ASSERT(roleCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(roleCallback.recipientInactivityDeadline == simTime() + 10); + roleCallback.scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(roleCallback.recipientInactivityDeadline == SIMTIME_MAX); + +} + +// A timeout DELBA can be aborted before its final fragment is transmitted. +// The matching generation is retired locally, and the returned ownership lets +// HCF release role-specific resources without rearming a timed-out agreement. +{ + TestOriginatorHandler originatorHandler; + originatorHandler.addEstablishedAgreement(peer1, 15); + auto originatorAgreement = originatorHandler.getAgreement(peer1, 15); + originatorAgreement->setBlockAckTimeoutValue(2); + originatorAgreement->calculateExpirationTime(); + originatorAgreement->markInactivityExpired(); + auto originatorGenerationId = originatorAgreement->getTransactionId(); + auto originatorFirstFragment = makeTaggedDelbaPacket("originatorExpiredDelbaFirst", peer1, 15, SequenceNumberCyclic(340), 0, true, originatorGenerationId); + auto originatorFinalFragment = makeTaggedDelbaPacket("originatorExpiredDelbaFinal", peer1, 15, SequenceNumberCyclic(340), 1, false, originatorGenerationId); + TestCallback originatorAbortCallback; + ASSERT(originatorHandler.processTransmittedDelba(originatorFirstFragment, &originatorAbortCallback) == nullptr); + auto originatorAbortResult = originatorHandler.processAbortedDelba(originatorFinalFragment, &originatorAbortCallback); + ASSERT(originatorAbortResult.handled); + ASSERT(originatorAbortResult.terminatedAgreement != nullptr); + ASSERT(originatorAbortResult.terminatedAgreement->getTransactionId() == originatorGenerationId); + ASSERT(originatorHandler.getAgreement(peer1, 15) == nullptr); + ASSERT(originatorAbortCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(originatorAbortCallback.cancelledTeardownGenerationIds.back() == originatorGenerationId); + auto staleOriginatorAbortResult = originatorHandler.processAbortedDelba(originatorFinalFragment, &originatorAbortCallback); + ASSERT(!staleOriginatorAbortResult.handled); + ASSERT(staleOriginatorAbortResult.terminatedAgreement == nullptr); + // Retiring the agreement is terminal for this generation: a late BlockAck + // must not resurrect it or publish a new inactivity deadline. + auto lateOriginatorBlockAck = makeShared(); + lateOriginatorBlockAck->setTransmitterAddress(peer1); + lateOriginatorBlockAck->setTidInfo(15); + lateOriginatorBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(340)); + TestCallback lateOriginatorActivityCallback; + originatorHandler.processReceivedBlockAck(lateOriginatorBlockAck, &lateOriginatorActivityCallback); + ASSERT(originatorHandler.getAgreement(peer1, 15) == nullptr); + ASSERT(lateOriginatorActivityCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(lateOriginatorActivityCallback.inactivityDeadlineUpdates.empty()); + delete originatorFirstFragment; + delete originatorFinalFragment; + + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 2; + TestCallback recipientSetupCallback; + auto recipientRequest = makeRequest(peer2, 16, 110, SequenceNumberCyclic(341), 32); + recipientRequest->setBlockAckTimeoutValue(2); + auto recipientAgreement = recipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientSetupCallback, &recipientSetupCallback); + ASSERT(recipientAgreement != nullptr); + recipientAgreement->markInactivityExpired(); + auto recipientGenerationId = recipientAgreement->getGenerationId(); + auto recipientFirstFragment = makeTaggedRecipientDelbaPacket("recipientExpiredDelbaFirst", peer2, 16, SequenceNumberCyclic(342), 0, true, recipientGenerationId); + auto recipientFinalFragment = makeTaggedRecipientDelbaPacket("recipientExpiredDelbaFinal", peer2, 16, SequenceNumberCyclic(342), 1, false, recipientGenerationId); + TestCallback recipientAbortCallback; + ASSERT(recipientHandler.processTransmittedDelba(recipientFirstFragment, &recipientAbortCallback) == nullptr); + auto recipientAbortResult = recipientHandler.processAbortedDelba(recipientFinalFragment, &recipientAbortCallback); + ASSERT(recipientAbortResult.handled); + ASSERT(recipientAbortResult.terminatedAgreement != nullptr); + ASSERT(recipientAbortResult.terminatedAgreement->getGenerationId() == recipientGenerationId); + ASSERT(recipientHandler.getAgreement(16, peer2) == nullptr); + ASSERT(recipientAbortCallback.recipientInactivityDeadline == SIMTIME_MAX); + ASSERT(recipientAbortCallback.cancelledTeardownGenerationIds.back() == recipientGenerationId); + auto staleRecipientAbortResult = recipientHandler.processAbortedDelba(recipientFinalFragment, &recipientAbortCallback); + ASSERT(!staleRecipientAbortResult.handled); + ASSERT(staleRecipientAbortResult.terminatedAgreement == nullptr); + // Likewise, late recipient traffic cannot recreate a retired agreement. + auto lateRecipientQosHeader = makeQosHeader(peer1, 16, SequenceNumberCyclic(343)); + lateRecipientQosHeader->setTransmitterAddress(peer2); + lateRecipientQosHeader->setAckPolicy(AckPolicy::BLOCK_ACK); + auto lateRecipientBlockAckReq = makeShared(); + lateRecipientBlockAckReq->setTransmitterAddress(peer2); + lateRecipientBlockAckReq->setTidInfo(16); + TestCallback lateRecipientActivityCallback; + recipientHandler.qosFrameReceived(lateRecipientQosHeader, &lateRecipientActivityCallback); + recipientHandler.blockAckReqReceived(lateRecipientBlockAckReq, &lateRecipientActivityCallback); + ASSERT(recipientHandler.getAgreement(16, peer2) == nullptr); + ASSERT(lateRecipientActivityCallback.recipientInactivityDeadline == SIMTIME_MAX); + ASSERT(lateRecipientActivityCallback.inactivityDeadlineUpdates.empty()); + delete recipientFirstFragment; + delete recipientFinalFragment; +} + // HCF routes recipient DELBAs to the recipient owner and rebuilds its // eligibility index when replacement or final teardown changes the generation. // The replacement callback removes queued stale siblings; the active current @@ -4959,6 +5134,40 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { hcf.configureManagementCallback(&managementCallback); hcf.observePendingQueue(&pendingQueue); + // HCF owns the terminal cleanup when a fragmented timeout DELBA is + // aborted before its final fragment is transmitted. The handler returns + // the retired agreement, allowing HCF to emit the deletion signal and to + // release recipient reorder resources exactly once. + TestSignalListener expiredDeletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &expiredDeletionListener); + originatorHandler->addEstablishedAgreement(peer1, 15); + auto expiredOriginatorAgreement = originatorHandler->getAgreement(peer1, 15); + expiredOriginatorAgreement->markInactivityExpired(); + auto expiredOriginatorGenerationId = expiredOriginatorAgreement->getTransactionId(); + auto expiredOriginatorDelba = makeTaggedDelbaPacket("hcfExpiredOriginatorDelba", peer1, 15, SequenceNumberCyclic(324), 1, false, expiredOriginatorGenerationId); + ASSERT(hcf.processDroppedTeardown(expiredOriginatorDelba)); + ASSERT(originatorHandler->getAgreement(peer1, 15) == nullptr); + delete expiredOriginatorDelba; + + auto expiredRecipientRequest = makeRequest(peer2, 15, 105, SequenceNumberCyclic(325), 32); + auto expiredRecipientAgreement = recipientHandler->processReceivedAddbaRequest(expiredRecipientRequest, &hcfRecipientPolicy, &managementCallback, &managementCallback); + ASSERT(expiredRecipientAgreement != nullptr); + expiredRecipientAgreement->markInactivityExpired(); + auto expiredRecipientGenerationId = expiredRecipientAgreement->getGenerationId(); + auto expiredRecipientDelba = makeTaggedRecipientDelbaPacket("hcfExpiredRecipientDelba", peer2, 15, SequenceNumberCyclic(326), 1, false, expiredRecipientGenerationId); + ASSERT(hcf.processDroppedTeardown(expiredRecipientDelba)); + ASSERT(recipientHandler->getAgreement(15, peer2) == nullptr); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(expiredDeletionListener.numSignals == 2); + delete expiredRecipientDelba; + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &expiredDeletionListener); + // Keep the following generation-lifecycle assertions focused on their + // own transactions rather than on the setup cleanup above. + hcf.numCancelledBlockAckTeardowns = 0; + hcf.cancelledBlockAckTeardownGenerationIds.clear(); + hcf.numRebuildEligibilityCalls = 0; + recipientDataService.numReorderingResets = 0; + auto stalePacket = makeTaggedRecipientDelbaPacket("hcfStaleRecipientDelba", peer2, 11, SequenceNumberCyclic(321), 0, false, firstGenerationId); pendingQueue.enqueuePacket(stalePacket); hcf.trackFrame(stalePacket, AC_BE); diff --git a/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test b/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test new file mode 100644 index 00000000000..741d6cc8975 --- /dev/null +++ b/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test @@ -0,0 +1,91 @@ +%description: +Exercise originator agreement expiry while synchronous callbacks erase current +and sibling agreements or install replacements. Check future and disabled deadlines. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +class ExpiryHandler : public OriginatorBlockAckAgreementHandler +{ + public: + void install(MacAddress peer, Tid tid, uint64_t generation, simtime_t timeout) { + auto key = std::make_pair(peer, tid); + delete blockAckAgreements[key]; + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(0), 64, true, false, 1, generation); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[key] = agreement; + } + void erase(MacAddress peer, Tid tid) { delete removeAgreement(peer, tid); } + bool contains(MacAddress peer, Tid tid) { return getAgreement(peer, tid) != nullptr; } +}; + +class ExpiryCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback +{ + public: + ExpiryHandler *handler = nullptr; + MacAddress peer; + int action = 0; + int sent = 0; + simtime_t deadline = 0; + virtual void transmitControlResponseFrame(Packet *, const Ptr&, Packet *, const Ptr&) override { throw cRuntimeError("Unexpected response"); } + virtual void scheduleInactivityTimer(BlockAckAgreementRole, simtime_t value) override { deadline = value; } + virtual void scheduleAddbaResponseTimer(simtime_t) override {} + virtual void cancelAddbaTransaction(uint64_t, Packet *) override {} + virtual void cancelBlockAckTeardown(bool, MacAddress, Tid, uint64_t, Packet *) override {} + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + auto delba = dynamicPtrCast(header); + ASSERT(delba != nullptr); + auto tid = delba->getTid(); + ASSERT(packet->getTag()->getGenerationId() == uint64_t(tid + 1)); + sent++; + auto result = handler->processAbortedDelba(packet, this); + ASSERT(result.handled); + ASSERT(result.terminatedAgreement != nullptr); + if (tid == 0) { + if (action == 1) + handler->erase(peer, 1); + if (action == 2) { + handler->install(peer, 0, 100, SimTime(10, SIMTIME_MS)); + handler->install(peer, 1, 101, SimTime(10, SIMTIME_MS)); + } + } + delete packet; + } +}; + +%activity: +for (int action = 0; action < 3; action++) { + ExpiryHandler handler; + ExpiryCallback callback; + callback.handler = &handler; + callback.peer = MacAddress("02:00:00:00:00:01"); + callback.action = action; + auto start = simTime(); + for (int tid = 0; tid < 2; tid++) + handler.install(callback.peer, tid, tid + 1, SimTime(1, SIMTIME_MS)); + handler.install(callback.peer, 2, 3, SimTime(20, SIMTIME_MS)); + handler.install(callback.peer, 3, 4, 0); + wait(SimTime(2, SIMTIME_MS)); + handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(handler.contains(callback.peer, 0) == (action >= 2)); + ASSERT(handler.contains(callback.peer, 1) == (action == 2)); + ASSERT(handler.contains(callback.peer, 2)); + ASSERT(handler.contains(callback.peer, 3)); + ASSERT(callback.deadline == start + SimTime(action >= 2 ? 12 : 20, SIMTIME_MS)); + handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(callback.sent == (action == 0 ? 2 : 1)); + for (int tid = 0; tid < 4; tid++) + handler.erase(callback.peer, tid); + handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(callback.deadline == SIMTIME_MAX); +} diff --git a/tests/unit/Ieee80211RecipientExpiryReentrant_1.test b/tests/unit/Ieee80211RecipientExpiryReentrant_1.test new file mode 100644 index 00000000000..e9c00024597 --- /dev/null +++ b/tests/unit/Ieee80211RecipientExpiryReentrant_1.test @@ -0,0 +1,89 @@ +%description: +Exercise recipient agreement expiry while synchronous callbacks erase current +and sibling agreements or install replacements. Check future and disabled deadlines. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +class ExpiryHandler : public RecipientBlockAckAgreementHandler +{ + public: + void install(MacAddress peer, Tid tid, uint64_t generation, simtime_t timeout) { + auto key = std::make_pair(peer, tid); + delete blockAckAgreements[key]; + auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(0), 64, timeout, generation); + agreement->calculateExpirationTime(); + blockAckAgreements[key] = agreement; + } + void erase(MacAddress peer, Tid tid) { delete removeAgreement(peer, tid); } + bool contains(MacAddress peer, Tid tid) { return getAgreement(tid, peer) != nullptr; } +}; + +class ExpiryCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback +{ + public: + ExpiryHandler *handler = nullptr; + MacAddress peer; + int action = 0; + int sent = 0; + simtime_t deadline = 0; + virtual void transmitControlResponseFrame(Packet *, const Ptr&, Packet *, const Ptr&) override { throw cRuntimeError("Unexpected response"); } + virtual void scheduleInactivityTimer(BlockAckAgreementRole, simtime_t value) override { deadline = value; } + virtual void scheduleAddbaResponseTimer(simtime_t) override {} + virtual void cancelAddbaTransaction(uint64_t, Packet *) override {} + virtual void cancelBlockAckTeardown(bool, MacAddress, Tid, uint64_t, Packet *) override {} + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + auto delba = dynamicPtrCast(header); + ASSERT(delba != nullptr); + auto tid = delba->getTid(); + ASSERT(packet->getTag()->getGenerationId() == uint64_t(tid + 1)); + sent++; + auto result = handler->processAbortedDelba(packet, this); + ASSERT(result.handled); + ASSERT(result.terminatedAgreement != nullptr); + if (tid == 0) { + if (action == 1) + handler->erase(peer, 1); + if (action == 2) { + handler->install(peer, 0, 100, SimTime(10, SIMTIME_MS)); + handler->install(peer, 1, 101, SimTime(10, SIMTIME_MS)); + } + } + delete packet; + } +}; + +%activity: +for (int action = 0; action < 3; action++) { + ExpiryHandler handler; + ExpiryCallback callback; + callback.handler = &handler; + callback.peer = MacAddress("02:00:00:00:00:01"); + callback.action = action; + auto start = simTime(); + for (int tid = 0; tid < 2; tid++) + handler.install(callback.peer, tid, tid + 1, SimTime(1, SIMTIME_MS)); + handler.install(callback.peer, 2, 3, SimTime(20, SIMTIME_MS)); + handler.install(callback.peer, 3, 4, 0); + wait(SimTime(2, SIMTIME_MS)); + handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(handler.contains(callback.peer, 0) == (action >= 2)); + ASSERT(handler.contains(callback.peer, 1) == (action == 2)); + ASSERT(handler.contains(callback.peer, 2)); + ASSERT(handler.contains(callback.peer, 3)); + ASSERT(callback.deadline == start + SimTime(action >= 2 ? 12 : 20, SIMTIME_MS)); + handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(callback.sent == (action == 0 ? 2 : 1)); + for (int tid = 0; tid < 4; tid++) + handler.erase(callback.peer, tid); + handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(callback.deadline == SIMTIME_MAX); +} From 11dfc1771270d1f1f25114e0577353ff986b2948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:45:55 +0200 Subject: [PATCH 35/91] ieee80211: add+fix: make fragment reassembly generation-safe Late fragments and sequence reuse must not combine bodies from different generations. Keep reassembly identity and sequence-space decisions coherent across retries, duplicates and replacement receive state. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.add+change.fix | test whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 6 + .../blockackreordering/BlockAckReordering.cc | 19 +- .../blockackreordering/BlockAckReordering.h | 6 + .../mac/fragmentation/BasicReassembly.cc | 274 +++++++++-- .../mac/fragmentation/BasicReassembly.h | 51 +- tests/unit/Ieee80211AddbaTransaction_1.test | 434 +++++++++++++++++- 6 files changed, 742 insertions(+), 48 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index a21d2239609..7b71a83cb6e 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -400,6 +400,12 @@ Notable backward incompatible changes are the following: safe when a bounded management queue drops a generated DELBA synchronously. Timers and traffic may differ from runs that kept extending an inactivity interval. +9. Fragment Identity Across Sequence Reuse + + Receive reassembly separates fragment generations and sequence spaces. Late + fragments, retries and reused sequence numbers no longer combine bodies from + unrelated transmissions; affected receive drops and deliveries may change. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 37967de00f0..6a449a9980b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -7,11 +7,24 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" +#include + #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { +BlockAckReordering::Fragments BlockAckReordering::sortFragmentsByFragmentNumber(const Fragments& fragments) +{ + auto sortedFragments = fragments; + std::stable_sort(sortedFragments.begin(), sortedFragments.end(), [](const Packet *first, const Packet *second) { + auto firstFragmentNumber = first->peekAtFront()->getFragmentNumber(); + auto secondFragmentNumber = second->peekAtFront()->getFragmentNumber(); + return firstFragmentNumber < secondFragmentNumber; + }); + return sortedFragments; +} + // // The recipient flushes received MSDUs from its receive buffer as described in this subclause. [...] // @@ -113,7 +126,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto fragments = it.second; if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) if (isComplete(fragments)) - completePrecedingMpdus[sequenceNumber] = fragments; + completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); } return completePrecedingMpdus; } @@ -140,7 +153,7 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder if (it != buffer.end()) { auto fragments = it->second; if (isComplete(fragments)) { - reorderBuffer[seqNum.get()] = fragments; + reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); return true; } } @@ -232,7 +245,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists } } } - return earliestFragments; + return sortFragmentsByFragmentNumber(earliestFragments); } BlockAckReordering::~BlockAckReordering() diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 09cf98a8d4b..10e27b4444d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -26,10 +26,16 @@ class INET_API BlockAckReordering typedef std::vector Fragments; typedef std::map ReorderBuffer; + // Values in ReorderBuffer results from processReceivedQoSFrame() and + // processReceivedBlockAckReq() contain complete fragment vectors ordered + // by ascending Fragment Number. The vectors only reorder packet pointers; + // packet ownership remains with the caller of those methods. + protected: std::map, ReceiveBuffer *> receiveBuffers; protected: + static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); ReorderBuffer collectCompletePrecedingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 95f4a35bfe3..91e2b939bb7 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -17,40 +17,202 @@ namespace ieee80211 { Register_Class(BasicReassembly); -/* - * FIXME this function needs a serious review - */ +BasicReassembly::SequenceSpaceKey BasicReassembly::getSequenceSpaceKey(const ContextKey& contextKey) const +{ + // IEEE Std 802.11-2024, Table 10-5 defines one baseline sequence-number + // space for frames not covered by a more specific space, and a separate + // space for individually addressed QoS Data indexed by RA and TID. + if (contextKey.type == ST_DATA_WITH_QOS && !contextKey.receiverAddress.isMulticast()) + return { contextKey.macAddress, contextKey.receiverAddress, contextKey.tid }; + else + return { contextKey.macAddress, MacAddress::UNSPECIFIED_ADDRESS, -1 }; +} + +SequenceNumber BasicReassembly::getRawSequenceNumber(ExtendedSequenceNumber extendedSequenceNumber) +{ + auto rawSequenceNumber = extendedSequenceNumber % NUM_SEQUENCE_NUMBERS; + if (rawSequenceNumber < 0) + rawSequenceNumber += NUM_SEQUENCE_NUMBERS; + return static_cast(rawSequenceNumber); +} + +BasicReassembly::SequenceObservation BasicReassembly::observeSequenceNumber(const SequenceSpaceKey& sequenceSpaceKey, SequenceNumber sequenceNumber) +{ + auto sequenceSpaceIt = sequenceSpacesMap.find(sequenceSpaceKey); + if (sequenceSpaceIt == sequenceSpacesMap.end()) { + sequenceSpacesMap.emplace(sequenceSpaceKey, SequenceSpaceValue{ sequenceNumber }); + return { sequenceNumber, false }; + } + + auto& highWatermark = sequenceSpaceIt->second.highWatermark; + auto highWaterSequenceNumber = getRawSequenceNumber(highWatermark); + auto highWaterCyclic = SequenceNumberCyclic(highWaterSequenceNumber); + auto sequenceCyclic = SequenceNumberCyclic(sequenceNumber); + + if (sequenceCyclic == highWaterCyclic) + return { highWatermark, false }; + + // SequenceNumberCyclic uses the IEEE 802.11 half-space ordering: a + // sequence number is ahead when its forward distance is in 1..2047. + if (highWaterCyclic < sequenceCyclic) { + auto forwardDistance = (sequenceNumber - highWaterSequenceNumber + NUM_SEQUENCE_NUMBERS) % NUM_SEQUENCE_NUMBERS; + ASSERT(forwardDistance > 0 && forwardDistance < NUM_SEQUENCE_NUMBERS / 2); + highWatermark += forwardDistance; + return { highWatermark, false }; + } + else if (sequenceCyclic < highWaterCyclic) { + auto backwardDistance = (highWaterSequenceNumber - sequenceNumber + NUM_SEQUENCE_NUMBERS) % NUM_SEQUENCE_NUMBERS; + ASSERT(backwardDistance > 0 && backwardDistance < NUM_SEQUENCE_NUMBERS / 2); + return { highWatermark - backwardDistance, false }; + } + + // Exactly half a sequence-number space has no ordering under the cyclic + // comparison. A fragmented MPDU must not be assigned a generation on + // guesswork. An unfragmented MPDU is safe to accept because it is never + // combined with buffered fragments; leave the high-water unchanged. + return { highWatermark, true }; +} + +void BasicReassembly::pruneExpiredSequenceNumbers(const SequenceSpaceKey& sequenceSpaceKey) +{ + auto sequenceSpaceIt = sequenceSpacesMap.find(sequenceSpaceKey); + if (sequenceSpaceIt == sequenceSpacesMap.end()) + return; + auto oldestRetainedGeneration = sequenceSpaceIt->second.highWatermark - NUM_SEQUENCE_NUMBERS; + for (auto contextIt = expiredSequenceNumbersMap.begin(); contextIt != expiredSequenceNumbersMap.end();) { + if (!(getSequenceSpaceKey(contextIt->first) == sequenceSpaceKey)) { + ++contextIt; + continue; + } + auto& expiredSequenceNumbers = contextIt->second; + for (auto sequenceIt = expiredSequenceNumbers.begin(); sequenceIt != expiredSequenceNumbers.end();) { + if (*sequenceIt < oldestRetainedGeneration) + sequenceIt = expiredSequenceNumbers.erase(sequenceIt); + else + ++sequenceIt; + } + if (expiredSequenceNumbers.empty()) + contextIt = expiredSequenceNumbersMap.erase(contextIt); + else + ++contextIt; + } +} + Packet *BasicReassembly::addFragment(Packet *packet) { const auto& header = packet->peekAtFront(); - // Frame is not fragmented - if (!header->getMoreFragments() && header->getFragmentNumber() == 0) - return packet; - // find entry for this frame - Key key; - key.macAddress = header->getTransmitterAddress(); - key.receiverAddress = header->getReceiverAddress(); - key.type = header->getType(); - key.tid = -1; + ContextKey contextKey; + contextKey.macAddress = header->getTransmitterAddress(); + contextKey.receiverAddress = header->getReceiverAddress(); + contextKey.type = header->getType(); + contextKey.tid = -1; if (header->getType() == ST_DATA_WITH_QOS) if (const Ptr& qosDataHeader = dynamicPtrCast(header)) - key.tid = qosDataHeader->getTid(); - key.seqNum = header->getSequenceNumber().get(); + contextKey.tid = qosDataHeader->getTid(); short fragNum = header->getFragmentNumber(); ASSERT(fragNum >= 0 && fragNum < MAX_NUM_FRAGMENTS); + bool isFragmented = header->getMoreFragments() || fragNum != 0; + auto sequenceSpaceKey = getSequenceSpaceKey(contextKey); + // This observed-epoch policy can separate only wraps for which sequence + // progress was received. A completely unseen wrap is indistinguishable + // from the same generation with the MAC metadata available here, so an + // equal raw sequence remains in the current observed epoch to preserve + // valid out-of-order completion. + auto sequenceObservation = observeSequenceNumber(sequenceSpaceKey, header->getSequenceNumber().get()); + + if (sequenceObservation.ambiguous) { + if (isFragmented) { + delete packet; + return nullptr; + } + return packet; + } + + Key key; + key.macAddress = contextKey.macAddress; + key.receiverAddress = contextKey.receiverAddress; + key.type = contextKey.type; + key.tid = contextKey.tid; + key.extendedSequenceNumber = sequenceObservation.extendedSequenceNumber; + auto it = fragmentsMap.find(key); - if (it != fragmentsMap.end() && it->second.expired) { - // A non-Retry fragment 0 can be a new MMPDU after sequence-number - // reuse. All other fragments of the expired MMPDU are discarded. - if (fragNum == 0 && !header->getRetry()) { + + auto expiredIt = expiredSequenceNumbersMap.find(contextKey); + if (isFragmented && expiredIt != expiredSequenceNumbersMap.end() && + expiredIt->second.find(key.extendedSequenceNumber) != expiredIt->second.end()) { + // A tombstone is scoped to the observed extended generation. Keep it + // even when a non-Retry fragment 0 arrives: without an observed + // sequence-space advance that fragment is not distinguishable from a + // late fragment of the expired MPDU. + delete packet; + return nullptr; + } + + // An unfragmented MPDU is never assembled with a fragmented MPDU. A new + // non-Retry frame retires an active same-generation reassembly, while a + // Retry leaves it available for its original fragments. + if (!isFragmented) { + if (it != fragmentsMap.end() && !header->getRetry()) { + for (auto fragment : it->second.fragments) + if (fragment != nullptr) + delete fragment; fragmentsMap.erase(it); - it = fragmentsMap.end(); } - else { - delete packet; - return nullptr; + pruneExpiredSequenceNumbers(sequenceSpaceKey); + return packet; + } + + // A same-raw active or expired entry in another observed epoch makes all + // fragments of the new epoch ambiguous, including fragment 0. Quarantine + // the new extended identity rather than allowing a delayed old fragment + // to seed a hybrid frame. IEEE Std 802.11-2024, 9.2.4.1.6, 10.3.2.14.2, + // and 10.5 + // define the modulo-4096 sequence identity, fragment identity, and + // receive-lifetime discard boundary; the following recovery policy keeps + // the ambiguous generation rejected while allowing a later observed + // generation to make progress. A fragmented non-Retry fragment 0 is the + // only marker that can retire older same-raw tombstones, and only when no + // active same-raw generation remains. A tombstone for the incoming + // generation is never cleared by this path. + bool hasOtherGeneration = false; + for (const auto& entry : fragmentsMap) { + const auto& otherKey = entry.first; + if (otherKey.getContextKey() == contextKey && + getRawSequenceNumber(otherKey.extendedSequenceNumber) == header->getSequenceNumber().get() && + otherKey.extendedSequenceNumber != key.extendedSequenceNumber) { + hasOtherGeneration = true; + break; + } + } + bool isNewGenerationMarker = fragNum == 0 && !header->getRetry(); + if (!hasOtherGeneration && isNewGenerationMarker && expiredIt != expiredSequenceNumbersMap.end()) { + for (auto sequenceIt = expiredIt->second.begin(); sequenceIt != expiredIt->second.end();) { + if (*sequenceIt < key.extendedSequenceNumber && + getRawSequenceNumber(*sequenceIt) == header->getSequenceNumber().get()) + sequenceIt = expiredIt->second.erase(sequenceIt); + else + ++sequenceIt; + } + if (expiredIt->second.empty()) + expiredSequenceNumbersMap.erase(expiredIt); + expiredIt = expiredSequenceNumbersMap.find(contextKey); + } + if (!hasOtherGeneration && expiredIt != expiredSequenceNumbersMap.end()) { + for (auto extendedSequenceNumber : expiredIt->second) { + if (extendedSequenceNumber != key.extendedSequenceNumber && + getRawSequenceNumber(extendedSequenceNumber) == header->getSequenceNumber().get()) { + hasOtherGeneration = true; + break; + } } } + if (hasOtherGeneration) { + expiredSequenceNumbersMap[contextKey].insert(key.extendedSequenceNumber); + pruneExpiredSequenceNumbers(sequenceSpaceKey); + delete packet; + return nullptr; + } + if (it == fragmentsMap.end()) { Value value; value.receptionStartTime = simTime(); @@ -61,18 +223,25 @@ Packet *BasicReassembly::addFragment(Packet *packet) // update entry uint16_t fragmentBit = 1 << fragNum; - value.receivedFragments |= fragmentBit; - if (!header->getMoreFragments()) - value.allFragments = (fragmentBit << 1) - 1; - if (!value.fragments[fragNum]) + if (!header->getMoreFragments()) { + if (value.terminalFragmentNumber == -1) + value.terminalFragmentNumber = fragNum; + else if (value.terminalFragmentNumber != fragNum) + value.hasContradictoryTerminalFragmentNumbers = true; + } + if (!value.fragments[fragNum]) { + value.receivedFragments |= fragmentBit; + if (!header->getMoreFragments() && value.allFragments == 0) + value.allFragments = (fragmentBit << 1) - 1; value.fragments[fragNum] = packet; + } else delete packet; // MacAddress txAddress = header->getTransmitterAddress(); // if all fragments arrived, return assembled frame - if (value.allFragments != 0 && value.allFragments == value.receivedFragments) { + if (!value.hasContradictoryTerminalFragmentNumbers && value.allFragments != 0 && value.allFragments == value.receivedFragments) { Defragmentation defragmentation; value.fragments.erase(std::remove(value.fragments.begin(), value.fragments.end(), nullptr), value.fragments.end()); auto defragmentedFrame = defragmentation.defragmentFrames(&value.fragments); @@ -82,47 +251,55 @@ Packet *BasicReassembly::addFragment(Packet *packet) for (auto fragment : value.fragments) delete fragment; fragmentsMap.erase(key); + pruneExpiredSequenceNumbers(sequenceSpaceKey); return defragmentedFrame; } - else + else { + pruneExpiredSequenceNumbers(sequenceSpaceKey); return nullptr; + } } simtime_t BasicReassembly::getNextExpirationTime() const { simtime_t nextExpirationTime = SIMTIME_MAX; for (const auto& entry : fragmentsMap) - if (!entry.second.expired) - nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); return nextExpirationTime; } std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) { std::vector expiredFragments; - for (auto& entry : fragmentsMap) { + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto& entry = *it; auto& value = entry.second; - if (!value.expired && currentTime >= value.receptionStartTime + maxReceiveLifetime) { + if (currentTime >= value.receptionStartTime + maxReceiveLifetime) { for (auto fragment : value.fragments) if (fragment != nullptr) expiredFragments.push_back(fragment); - value.fragments.clear(); - value.receivedFragments = 0; - value.allFragments = 0; - value.expired = true; + auto contextKey = entry.first.getContextKey(); + auto sequenceSpaceKey = getSequenceSpaceKey(contextKey); + expiredSequenceNumbersMap[contextKey].insert(entry.first.extendedSequenceNumber); + it = fragmentsMap.erase(it); + pruneExpiredSequenceNumbers(sequenceSpaceKey); } + else + ++it; } return expiredFragments; } void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) { - for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { - auto sequenceNumber = it->first.seqNum; - bool isInSequenceRange = startSeqNumber <= endSeqNumber ? + auto isInSequenceRange = [startSeqNumber, endSeqNumber](int sequenceNumber) { + return startSeqNumber <= endSeqNumber ? sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; - if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange) { + }; + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto sequenceNumber = getRawSequenceNumber(it->first.extendedSequenceNumber); + if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange(sequenceNumber)) { for (auto fragment : it->second.fragments) delete fragment; it = fragmentsMap.erase(it); @@ -130,6 +307,23 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb else it++; } + + for (auto it = expiredSequenceNumbersMap.begin(); it != expiredSequenceNumbersMap.end();) { + if (it->first.macAddress == address && it->first.tid == tid) { + for (auto sequenceIt = it->second.begin(); sequenceIt != it->second.end();) { + if (isInSequenceRange(getRawSequenceNumber(*sequenceIt))) + sequenceIt = it->second.erase(sequenceIt); + else + ++sequenceIt; + } + if (it->second.empty()) + it = expiredSequenceNumbersMap.erase(it); + else + ++it; + } + else + ++it; + } } BasicReassembly::~BasicReassembly() diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index bb0dd20ba71..7f9ee6a01b0 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,6 +8,8 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include +#include #include #include "inet/common/packet/Packet.h" @@ -21,13 +23,37 @@ namespace ieee80211 { class INET_API BasicReassembly : public IReassembly, public cObject { protected: + static constexpr int NUM_SEQUENCE_NUMBERS = 1 << 12; + using ExtendedSequenceNumber = int64_t; + + struct ContextKey { + MacAddress macAddress; + MacAddress receiverAddress; + Ieee80211FrameType type; + Tid tid; + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid); } + bool operator==(const ContextKey& other) const { return asTuple() == other.asTuple(); } + bool operator<(const ContextKey& other) const { return asTuple() < other.asTuple(); } + }; + struct SequenceSpaceKey { + MacAddress transmitterAddress; + MacAddress receiverAddress; + Tid tid; + auto asTuple() const { return std::tie(transmitterAddress, receiverAddress, tid); } + bool operator==(const SequenceSpaceKey& other) const { return asTuple() == other.asTuple(); } + bool operator<(const SequenceSpaceKey& other) const { return asTuple() < other.asTuple(); } + }; + struct SequenceSpaceValue { + ExtendedSequenceNumber highWatermark; + }; struct Key { MacAddress macAddress; MacAddress receiverAddress; Ieee80211FrameType type; Tid tid; - SequenceNumber seqNum; - auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, seqNum); } + ExtendedSequenceNumber extendedSequenceNumber; + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, extendedSequenceNumber); } + ContextKey getContextKey() const { return { macAddress, receiverAddress, type, tid }; } bool operator==(const Key& other) const { return asTuple() == other.asTuple(); } bool operator<(const Key& other) const { return asTuple() < other.asTuple(); } }; @@ -35,13 +61,32 @@ class INET_API BasicReassembly : public IReassembly, public cObject std::vector fragments; uint16_t receivedFragments = 0; // each bit corresponds to a fragment number uint16_t allFragments = 0; // bits for all fragments set to one (0..numFragments-1); 0 means unfilled + int terminalFragmentNumber = -1; + bool hasContradictoryTerminalFragmentNumbers = false; simtime_t receptionStartTime; - bool expired = false; }; typedef std::map FragmentsMap; + typedef std::map SequenceSpacesMap; + using ExpiredSequenceNumbers = std::set; + // Generation-scoped tombstones reject late or ambiguous fragments and + // retain identities across one raw-sequence reuse. Older entries are + // pruned as the observed sequence-space high-water advances. + typedef std::map ExpiredSequenceNumbersMap; FragmentsMap fragmentsMap; + SequenceSpacesMap sequenceSpacesMap; + ExpiredSequenceNumbersMap expiredSequenceNumbersMap; simtime_t maxReceiveLifetime; + struct SequenceObservation { + ExtendedSequenceNumber extendedSequenceNumber; + bool ambiguous = false; + }; + + SequenceSpaceKey getSequenceSpaceKey(const ContextKey& contextKey) const; + SequenceObservation observeSequenceNumber(const SequenceSpaceKey& sequenceSpaceKey, SequenceNumber sequenceNumber); + void pruneExpiredSequenceNumbers(const SequenceSpaceKey& sequenceSpaceKey); + static SequenceNumber getRawSequenceNumber(ExtendedSequenceNumber extendedSequenceNumber); + public: BasicReassembly() : maxReceiveLifetime(SimTime(512 * 1024, SIMTIME_US)) {} BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 8725e60ec75..5e5e3ce3e3e 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -11,6 +11,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/common/TimeTag_m.h" #include "inet/common/LabelsTag_m.h" #include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" @@ -531,6 +532,29 @@ class TestInProgressFrames : public InProgressFrames } }; +class TestBasicReassembly : public BasicReassembly +{ + public: + using BasicReassembly::BasicReassembly; + + size_t getNumActiveReassemblies() const { return fragmentsMap.size(); } + bool hasContradictoryTerminalFragmentNumbers() const { + ASSERT(fragmentsMap.size() == 1); + return fragmentsMap.begin()->second.hasContradictoryTerminalFragmentNumbers; + } + uint16_t getAllFragments() const { + ASSERT(fragmentsMap.size() == 1); + return fragmentsMap.begin()->second.allFragments; + } + size_t getNumExpiredContexts() const { return expiredSequenceNumbersMap.size(); } + size_t getNumExpiredSequenceNumbers() const { + size_t count = 0; + for (const auto& entry : expiredSequenceNumbersMap) + count += entry.second.size(); + return count; + } +}; + class TestEdcaf : public Edcaf { public: @@ -2132,8 +2156,9 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(fragmentedCallback.managementPackets.size() == 1); delete fragments; - // Seeing the final fragment first does not imply completeness. Dispatch - // occurs only when the missing lower-numbered fragment later arrives. + // IEEE Std 802.11-2024, 10.5 permits fragments to be received out of + // order. The final fragment therefore starts a reassembly and fragment 0 + // completes it when it arrives later. auto outOfOrderFragments = makeFragments("outOfOrderAddbaRequest", 4, 61); for (auto& fragment : *outOfOrderFragments) fragment = convertToWireRepresentation(fragment); @@ -2143,6 +2168,13 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(fragmentedCallback.managementPackets.size() == 1); auto outOfOrderCompleteReception = receiveFragment(outOfOrderFragments->at(0)); ASSERT(outOfOrderCompleteReception.completeHeader != nullptr); + ASSERT(outOfOrderCompleteReception.completeHeader->getFragmentNumber() == 0); + ASSERT(!outOfOrderCompleteReception.completeHeader->getMoreFragments()); + auto outOfOrderRequest = dynamicPtrCast(outOfOrderCompleteReception.completeHeader); + ASSERT(outOfOrderRequest != nullptr); + ASSERT(outOfOrderRequest->getTid() == 4); + ASSERT(outOfOrderRequest->getDialogToken() == 74); + ASSERT(outOfOrderRequest->getStartingSequenceNumber() == SequenceNumberCyclic(104)); hcf.processReceivedManagementFrame(outOfOrderCompleteReception.completeHeader); ASSERT(recipientHandler->getAgreement(4, peer2) != nullptr); ASSERT(fragmentedCallback.managementPackets.size() == 2); @@ -2369,6 +2401,83 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; delete discardedNewWindowFrames.front(); } +// IEEE Std 802.11-2024, 10.4 and 10.5: fragments are transmitted from +// fragment 0 upward and the destination reconstructs an MMPDU in Fragment +// Number order. BlockAckReordering receives fragments in arrival order, so +// both immediate and BAR-triggered complete vectors must be normalized before +// they reach BasicReassembly. +{ + auto makeQosFragment = [&](const char *name, SequenceNumberCyclic sequenceNumber, Tid tid, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, tid, sequenceNumber); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto assertFragmentOrder = [](const BlockAckReordering::Fragments& fragments) { + ASSERT(fragments.size() == 4); + for (int fragmentNumber = 0; fragmentNumber < 4; fragmentNumber++) + ASSERT(fragments.at(fragmentNumber)->peekAtFront()->getFragmentNumber() == fragmentNumber); + }; + auto reassembleOrderedFragments = [&](const BlockAckReordering::Fragments& fragments, const std::vector& expectedPayload) { + TestBasicReassembly reassembly; + Packet *completedPacket = nullptr; + int numCompletedPackets = 0; + for (auto fragment : fragments) { + auto packet = reassembly.addFragment(fragment); + if (packet != nullptr) { + completedPacket = packet; + numCompletedPackets++; + } + } + ASSERT(numCompletedPackets == 1); + ASSERT(completedPacket != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + auto completedHeader = completedPacket->peekAtFront(); + ASSERT(completedPacket->peekAt(completedHeader->getChunkLength(), B(expectedPayload.size()))->getBytes() == expectedPayload); + delete completedPacket; + }; + + RecipientBlockAckAgreement immediateAgreement(peer1, 6, SequenceNumberCyclic(100), 64, 0); + TestBlockAckReordering immediateReordering; + BlockAckReordering::ReorderBuffer immediateFrames; + for (int fragmentNumber : { 1, 2, 3, 0 }) { + auto packet = makeQosFragment("immediateOutOfOrderFragment", SequenceNumberCyclic(100), 6, fragmentNumber, fragmentNumber != 3, 0x40 + fragmentNumber); + auto dataHeader = packet->peekAtFront(); + auto frames = immediateReordering.processReceivedQoSFrame(&immediateAgreement, packet, dataHeader); + if (fragmentNumber == 0) + immediateFrames = frames; + else + ASSERT(frames.empty()); + } + ASSERT(immediateFrames.size() == 1); + auto immediateFragments = immediateFrames.at(100); + assertFragmentOrder(immediateFragments); + reassembleOrderedFragments(immediateFragments, { 0x40, 0x41, 0x42, 0x43 }); + + RecipientBlockAckAgreement barAgreement(peer1, 6, SequenceNumberCyclic(200), 64, 0); + TestBlockAckReordering barReordering; + for (int fragmentNumber : { 1, 2, 3, 0 }) { + auto packet = makeQosFragment("barOutOfOrderFragment", SequenceNumberCyclic(201), 6, fragmentNumber, fragmentNumber != 3, 0x50 + fragmentNumber); + auto dataHeader = packet->peekAtFront(); + ASSERT(barReordering.processReceivedQoSFrame(&barAgreement, packet, dataHeader).empty()); + } + auto barRequest = makeShared(); + barRequest->setTransmitterAddress(peer1); + barRequest->setReceiverAddress(peer2); + barRequest->setTidInfo(6); + barRequest->setStartingSequenceNumber(SequenceNumberCyclic(201)); + auto barFrames = barReordering.processReceivedBlockAckReq(&barAgreement, barRequest); + ASSERT(barFrames.size() == 1); + auto barFragments = barFrames.at(201); + assertFragmentOrder(barFragments); + reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); +} + // The recipient data service owns reorder-buffer reset observability: every // buffered MPDU is reported exactly once before it is deleted. { @@ -4801,6 +4910,327 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); } +// IEEE Std 802.11-2024, 10.5 requires reconstruction in Fragment Number +// order. A later fragment can arrive before fragment 0, and the receiver's +// observed sequence-number generation keeps a later sequence reuse separate. +{ + TestBasicReassembly reassembly; + auto makeResponseFragments = [&](const char *name, uint8_t dialogToken, uint16_t statusCode, const std::vector& payload, int sequenceNumber, int fragmentCount) { + auto response = makeResponse(peer1, 3, dialogToken, statusCode); + response->setReceiverAddress(peer2); + response->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto packet = new Packet(name, response); + packet->insertAtBack(makeShared(payload)); + packet->insertAtBack(makeShared()); + auto managementHeaderLength = makeShared()->getChunkLength(); + auto bodyLength = (int)(response->getChunkLength() - managementHeaderLength).get() + (int)payload.size(); + Fragmentation fragmentation; + auto fragmentSizes = std::vector({ 8, bodyLength - 8 }); + if (fragmentCount == 3) + fragmentSizes = { 4, 4, bodyLength - 8 }; + ASSERT(fragmentCount == (int)fragmentSizes.size()); + return fragmentation.fragmentFrame(packet, fragmentSizes); + }; + + const std::vector outOfOrderPayload = { 0x14, 0x15, 0x16, 0x17 }; + auto outOfOrderFragments = makeResponseFragments("reassemblyOutOfOrderResponse", 23, 5, outOfOrderPayload, 4094, 2); + ASSERT(reassembly.addFragment(outOfOrderFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto outOfOrderExpiration = reassembly.getNextExpirationTime(); + ASSERT(outOfOrderExpiration != SIMTIME_MAX); + auto retryOutOfOrderFirst = outOfOrderFragments->at(0)->dup(); + auto retryOutOfOrderHeader = retryOutOfOrderFirst->removeAtFront(); + retryOutOfOrderHeader->setRetry(true); + retryOutOfOrderFirst->insertAtFront(retryOutOfOrderHeader); + auto outOfOrderCompleted = reassembly.addFragment(retryOutOfOrderFirst); + ASSERT(outOfOrderCompleted != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + auto outOfOrderHeader = outOfOrderCompleted->peekAtFront(); + auto outOfOrderResponse = dynamicPtrCast(outOfOrderHeader); + ASSERT(outOfOrderResponse != nullptr); + ASSERT(outOfOrderResponse->getDialogToken() == 23); + ASSERT(outOfOrderResponse->getStatusCode() == 5); + ASSERT(outOfOrderResponse->getRetry()); + ASSERT(outOfOrderCompleted->peekAt(outOfOrderResponse->getChunkLength(), B(outOfOrderPayload.size()))->getBytes() == outOfOrderPayload); + delete outOfOrderCompleted; + delete outOfOrderFragments->at(0); + delete outOfOrderFragments; + + // A sequence number exactly half a space away has no cyclic ordering. + // Neither an independently accepted unfragmented frame nor a rejected + // fragmented frame at that boundary may disturb an active generation. + const std::vector halfSpacePayload = { 0x18, 0x19, 0x1a }; + auto preservedFragments = makeResponseFragments("reassemblyPreservedResponse", 25, 6, halfSpacePayload, 4093, 2); + ASSERT(reassembly.addFragment(preservedFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto halfSpaceStandalone = makeTaggedManagementPacket("reassemblyHalfSpaceStandalone", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(2046), 0, false, 0); + ASSERT(reassembly.addFragment(halfSpaceStandalone) == halfSpaceStandalone); + delete halfSpaceStandalone; + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto halfSpaceFragments = makeResponseFragments("reassemblyHalfSpaceResponse", 27, 7, halfSpacePayload, 2046, 2); + ASSERT(reassembly.addFragment(halfSpaceFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto preservedCompleted = reassembly.addFragment(preservedFragments->at(0)); + ASSERT(preservedCompleted != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + delete preservedCompleted; + delete preservedFragments; + delete halfSpaceFragments->at(1); + delete halfSpaceFragments; + + // The old final fragment remains buffered while the receiver observes the + // numeric wrap and enough baseline sequence-space progress to identify a + // reused raw number. A delayed old fragment 0 and the real new fragment 0 + // are indistinguishable, so their new extended generation is quarantined. + const std::vector oldPayload = { 0x20, 0x21, 0x22, 0x23 }; + auto oldFragments = makeResponseFragments("reassemblyOldResponse", 17, 1, oldPayload, 4095, 2); + auto lateOldFirstFragment = oldFragments->at(0)->dup(); + auto lateOldRetryFragment = oldFragments->at(0)->dup(); + auto lateOldRetryHeader = lateOldRetryFragment->removeAtFront(); + lateOldRetryHeader->setRetry(true); + lateOldRetryFragment->insertAtFront(lateOldRetryHeader); + ASSERT(reassembly.addFragment(oldFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto oldExpiration = reassembly.getNextExpirationTime(); + wait(1e-9); + for (int sequenceNumber = 0; sequenceNumber <= 2048; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblySequenceAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(reassembly.addFragment(standalone) == standalone); + delete standalone; + } + + const std::vector newPayload = { 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6 }; + auto newFragments = makeResponseFragments("reassemblyNewResponse", 29, 37, newPayload, 4095, 3); + ASSERT(reassembly.addFragment(lateOldFirstFragment) == nullptr); + ASSERT(reassembly.addFragment(lateOldRetryFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + ASSERT(reassembly.addFragment(newFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.addFragment(newFragments->at(1)) == nullptr); + ASSERT(reassembly.addFragment(newFragments->at(2)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiredOld = reassembly.removeExpiredFragments(oldExpiration); + ASSERT(expiredOld.size() == 1); + delete expiredOld.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 2); + delete oldFragments->at(0); + delete oldFragments; + delete newFragments; + + // Once the old active entry and the quarantined generation are gone, the + // receiver can observe one more modulo-4096 wrap. The later non-Retry + // fragment 0 retires the older same-raw tombstone without an external + // purge, and only the later generation's bytes are reconstructed. + for (int sequenceNumber = 0; sequenceNumber <= 2048; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblySecondSequenceAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(reassembly.addFragment(standalone) == standalone); + delete standalone; + } + auto recoveredFragments = makeResponseFragments("reassemblyRecoveredResponse", 29, 37, newPayload, 4095, 3); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + ASSERT(reassembly.addFragment(recoveredFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 0); + ASSERT(reassembly.addFragment(recoveredFragments->at(1)) == nullptr); + auto completed = reassembly.addFragment(recoveredFragments->at(2)); + ASSERT(completed != nullptr); + auto completedHeader = completed->peekAtFront(); + auto completedResponse = dynamicPtrCast(completedHeader); + ASSERT(completedResponse != nullptr); + ASSERT(completedResponse->getDialogToken() == 29); + ASSERT(completedResponse->getStatusCode() == 37); + ASSERT(completedResponse->getSequenceNumber() == SequenceNumberCyclic(4095)); + ASSERT(completedResponse->getFragmentNumber() == 0); + ASSERT(!completedResponse->getMoreFragments()); + ASSERT(completed->peekAt(completedResponse->getChunkLength(), B(newPayload.size()))->getBytes() == newPayload); + delete completed; + delete recoveredFragments; +} + +// A terminal duplicate on an occupied nonterminal slot is still observed for +// contradiction tracking, but cannot establish an affirmative completion mask. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstFragment = makeTaggedManagementPacket("reassemblyDuplicateTerminalFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 0, true, 1); + auto secondFragment = makeTaggedManagementPacket("reassemblyDuplicateTerminalSecond", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 1, true, 2); + auto duplicateTerminal = makeTaggedManagementPacket("reassemblyDuplicateTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 1, false, 3); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.getAllFragments() == 0); + ASSERT(reassembly.addFragment(secondFragment) == nullptr); + ASSERT(reassembly.addFragment(duplicateTerminal) == nullptr); + ASSERT(!reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getAllFragments() == 0); + ASSERT(reassembly.getNextExpirationTime() == expiration); + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 2); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + for (auto fragment : expiredFragments) + delete fragment; +} + +// A terminal duplicate on an occupied slot with a different number from the +// retained terminal marker invalidates completion, even when all slots arrive. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto retainedTerminal = makeTaggedManagementPacket("reassemblyRetainedTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 2, false, 1); + auto retainedFragment = makeTaggedManagementPacket("reassemblyRetainedFragment", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 1, true, 2); + auto duplicateTerminal = makeTaggedManagementPacket("reassemblyContradictoryDuplicateTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 1, false, 3); + auto firstFragment = makeTaggedManagementPacket("reassemblyContradictoryDuplicateFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 0, true, 4); + ASSERT(reassembly.addFragment(retainedTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.getAllFragments() == 7); + ASSERT(reassembly.addFragment(retainedFragment) == nullptr); + ASSERT(reassembly.addFragment(duplicateTerminal) == nullptr); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getAllFragments() == 7); + ASSERT(reassembly.getNextExpirationTime() == expiration); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 3); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + for (auto fragment : expiredFragments) + delete fragment; +} + +// Contradictory terminal markers invalidate completion independent of their +// arrival order. The invalid entry remains owned until receive-lifetime +// expiry, which returns each uniquely retained fragment to the caller. +for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) }) { + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstTerminal = makeTaggedManagementPacket("reassemblyContradictoryFirstTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), terminalOrder.first, false, 1); + auto secondTerminal = makeTaggedManagementPacket("reassemblyContradictorySecondTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), terminalOrder.second, false, 2); + auto firstFragment = makeTaggedManagementPacket("reassemblyContradictoryFirstFragment", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), 0, true, 3); + ASSERT(reassembly.addFragment(firstTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(!reassembly.hasContradictoryTerminalFragmentNumbers()); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.addFragment(secondTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 3); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNextExpirationTime() == SIMTIME_MAX); + for (auto fragment : expiredFragments) + delete fragment; +} + +// Expired reassembly stores exact observed generations, rejects late +// fragments, and does not clear a tombstone when a same-generation fragment 0 +// arrives without an observed sequence-space advance. Unfragmented frames are +// accepted independently and never mixed with the tombstone. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstFragment = makeTaggedManagementPacket("reassemblyExpiredFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 0, true, 0); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(2, SIMTIME_US)); + ASSERT(expiredFragments.size() == 1); + delete expiredFragments.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + + // A receive-lifetime tombstone does not require an external purge for + // recovery. Once the receiver observes a later generation in the same + // sequence space, its non-Retry fragment 0 retires the older tombstone. + { + TestBasicReassembly wrapRecovery(SimTime(1, SIMTIME_US)); + auto expiredFirst = makeTaggedManagementPacket("reassemblyExpiredWrapFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, true, 0); + ASSERT(wrapRecovery.addFragment(expiredFirst) == nullptr); + auto expiration = wrapRecovery.getNextExpirationTime(); + auto expired = wrapRecovery.removeExpiredFragments(expiration); + ASSERT(expired.size() == 1); + delete expired.front(); + for (int sequenceNumber = 431; sequenceNumber <= 2479; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblyExpiredWrapAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(wrapRecovery.addFragment(standalone) == standalone); + delete standalone; + } + auto recoveredFirst = makeTaggedManagementPacket("reassemblyExpiredWrapRecoveredFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, true, 1); + ASSERT(wrapRecovery.addFragment(recoveredFirst) == nullptr); + ASSERT(wrapRecovery.getNumExpiredSequenceNumbers() == 0); + ASSERT(wrapRecovery.getNumActiveReassemblies() == 1); + // Leave the recovered, incomplete reassembly for the destructor to own + // and clean up; completion is covered by the exact-payload recovery test above. + } + + auto lateFinalFragment = makeTaggedManagementPacket("reassemblyLateFinal", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 1, false, 0, true); + ASSERT(reassembly.addFragment(lateFinalFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + auto reusedFirstFragment = makeTaggedManagementPacket("reassemblyReusedFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 0, true, 0); + ASSERT(reassembly.addFragment(reusedFirstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + auto unfragmentedPacket = makeTaggedManagementPacket("reassemblyUnfragmentedAfterExpiry", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, false, 0); + ASSERT(reassembly.addFragment(unfragmentedPacket) == unfragmentedPacket); + delete unfragmentedPacket; + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + for (int sequenceNumber = 0; sequenceNumber < 64; sequenceNumber++) { + auto fragment = makeTaggedManagementPacket("reassemblyManyExpiredFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(500 + sequenceNumber), 0, true, 0); + ASSERT(reassembly.addFragment(fragment) == nullptr); + } + ASSERT(reassembly.getNumActiveReassemblies() == 64); + auto manyExpiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(3, SIMTIME_US)); + ASSERT(manyExpiredFragments.size() == 64); + for (auto fragment : manyExpiredFragments) + delete fragment; + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNextExpirationTime() == SIMTIME_MAX); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 65); + auto otherContextFirstFragment = makeTaggedManagementPacket("reassemblyOtherContextFirst", ST_ACTION, peer2, peer3, + SequenceNumberCyclic(600), 0, true, 0); + ASSERT(reassembly.addFragment(otherContextFirstFragment) == nullptr); + auto otherContextExpiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(4, SIMTIME_US)); + ASSERT(otherContextExpiredFragments.size() == 1); + delete otherContextExpiredFragments.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 2); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 66); + reassembly.purge(peer2, -1, 0, 4095); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 65); + reassembly.purge(peer1, -1, 0, 4095); + ASSERT(reassembly.getNumExpiredContexts() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 0); +} + // An originator inactivity DELBA retains the agreement transaction identity. // Once that agreement is replaced, every stale fragment is rejected while a // final DELBA carrying the current generation removes the replacement once. From a85c43c235cf3ead0ae7ea8483c902f691a4105f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:47:16 +0200 Subject: [PATCH 36/91] ieee80211: add+fix: enforce receive lifetime during reordering Incomplete fragmented bodies must expire from their first reception even when Block Ack reordering delays delivery. Preserve receive lifetime across buffering and reject late fragments without delivering a hybrid or expired body. Ad-hoc MacQos run 1 changes three values. There are three extra receive drops and three fewer deliveries. A byte trace isolates one BasicBlockAck bitmap change after prevented late completion stops advancing old-sequence acknowledgment state. All packet times and lengths remain identical. The user accepted these exact source-owned expectations on 2026-09-15. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.add+change.fix | fingerprint test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 6 + doc/src/migration-guide/index.rst | 12 + .../blockackreordering/BlockAckReordering.cc | 79 ++++-- .../blockackreordering/BlockAckReordering.h | 11 +- .../mac/blockackreordering/ReceiveBuffer.cc | 103 +++++++- .../mac/blockackreordering/ReceiveBuffer.h | 26 +- .../ieee80211/mac/contract/IReassembly.h | 9 +- .../mac/fragmentation/BasicReassembly.cc | 7 +- .../mac/fragmentation/BasicReassembly.h | 2 +- .../mac/recipient/RecipientMacDataService.cc | 5 +- .../mac/recipient/RecipientMacDataService.ned | 2 +- .../recipient/RecipientQosMacDataService.cc | 89 +++++-- .../recipient/RecipientQosMacDataService.h | 7 +- .../recipient/RecipientQosMacDataService.ned | 2 +- tests/fingerprint/examples.csv | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 242 +++++++++++++++++- 16 files changed, 536 insertions(+), 68 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 7b71a83cb6e..c58fffc7094 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -406,6 +406,12 @@ Notable backward incompatible changes are the following: fragments, retries and reused sequence numbers no longer combine bodies from unrelated transmissions; affected receive drops and deliveries may change. +10. Receive Lifetime in Block Ack Reordering + + Receive lifetime now remains anchored to first reception while fragments wait in + Block Ack reordering. A later stage no longer gives an old incomplete body a fresh + lifetime, so delayed or incomplete bodies may be dropped earlier. + 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 1d1bfac9ff2..958e7845cb6 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -166,6 +166,18 @@ peer/TID/generation values as the work list and relookup the live state before u after a callback; incrementing a map iterator before calling out does not protect a removed sibling. +Receive Lifetime in Block Ack Reordering +---------------------------------------- + +Custom receive/reordering implementations must retain the first-reception time of a +fragmented body while it waits in a Block Ack reorder buffer. Moving fragments between +receive stages must not restart their lifetime. Expire incomplete state and reject late +fragments before delivering a reassembled body. ``IReassembly::purge()`` now returns +owned packets; callers must report their final drop and delete them. Use +``BlockAckReordering::processReceivedQoSFrameWithResult()`` to handle released frames +and tombstoned fragments. Negative ``maxReceiveLifetime`` values are rejected; zero is a +valid immediate-expiry setting. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 6a449a9980b..35d2e86507c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -28,14 +28,16 @@ BlockAckReordering::Fragments BlockAckReordering::sortFragmentsByFragmentNumber( // // The recipient flushes received MSDUs from its receive buffer as described in this subclause. [...] // -BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceivedQoSFrameWithResult(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) { + QosFrameProcessingResult result; ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); // The reception of QoS data frames using Normal Ack policy shall not be used by the // recipient to reset the timer to detect Block Ack timeout (see 10.5.4). // This allows the recipient to delete the Block Ack if the originator does not switch // back to using Block Ack. - if (receiveBuffer->insertFrame(dataPacket, dataHeader)) { + auto insertionResult = receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); + if (insertionResult == ReceiveBuffer::FrameInsertionResult::INSERTED) { if (dataHeader->getAckPolicy() == BLOCK_ACK) agreement->blockAckPolicyFrameReceived(dataHeader); auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); @@ -45,20 +47,41 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(Re // sequence number shall be passed up to the next MAC process. if (receiveBuffer->isFull()) { passedUp(agreement, receiveBuffer, earliestSequenceNumber); - return ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + return result; } // If, after an MPDU is received, the receive buffer is not full, but the sequence number of the complete MSDU or // A-MSDU in the buffer with the lowest sequence number is equal to the NextExpectedSequenceNumber for // that Block Ack agreement, then the MPDU shall be passed up to the next MAC process. else if (earliestSequenceNumber == receiveBuffer->getNextExpectedSequenceNumber()) { passedUp(agreement, receiveBuffer, earliestSequenceNumber); - return ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + return result; } } } - else + else { + if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) { + // A later fragment of a receive-timer-expired MPDU is still a + // received BLOCK_ACK frame, even though the fragment is rejected + // by the tombstone and must be reported to the service for drop + // observability. + if (dataHeader->getAckPolicy() == BLOCK_ACK) + agreement->blockAckPolicyFrameReceived(dataHeader); + result.tombstonedFragments.push_back(dataPacket); + return result; + } delete dataPacket; - return ReorderBuffer({}); + } + return result; +} + +BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +{ + auto result = processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); + for (auto packet : result.tombstonedFragments) + delete packet; + return result.frames; } // @@ -125,7 +148,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto sequenceNumber = it.first; auto fragments = it.second; if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) - if (isComplete(fragments)) + if (ReceiveBuffer::isComplete(fragments)) completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); } return completePrecedingMpdus; @@ -152,7 +175,7 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder auto it = buffer.find(seqNum.get()); if (it != buffer.end()) { auto fragments = it->second; - if (isComplete(fragments)) { + if (ReceiveBuffer::isComplete(fragments)) { reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); return true; } @@ -168,19 +191,6 @@ void BlockAckReordering::releaseReceiveBuffer(RecipientBlockAckAgreement *agreem } } -bool BlockAckReordering::isComplete(const std::vector& fragments) -{ - int largestFragmentNumber = -1; - std::set fragNums; // possible duplicate frames - for (auto fragment : fragments) { - const auto& header = fragment->peekAtFront(); - if (!header->getMoreFragments()) - largestFragmentNumber = header->getFragmentNumber(); - fragNums.insert(header->getFragmentNumber()); - } - return largestFragmentNumber != -1 && largestFragmentNumber + 1 == (int)fragNums.size(); -} - ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement) { SequenceNumberCyclic startingSequenceNumber = agreement->getStartingSequenceNumber(); @@ -198,6 +208,29 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } +simtime_t BlockAckReordering::getNextExpirationTime() const +{ + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& [id, receiveBuffer] : receiveBuffers) + nextExpirationTime = std::min(nextExpirationTime, receiveBuffer->getNextExpirationTime(maxReceiveLifetime)); + return nextExpirationTime; +} + +std::vector BlockAckReordering::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (const auto& [id, receiveBuffer] : receiveBuffers) { + // IEEE Std 802.11-2024, 10.5 requires incomplete fragmented MPDUs + // to be discarded when dot11MaxReceiveLifetime elapses. The receive + // timer does not advance NextExpectedSequenceNumber or alter the + // Block Ack record; those variables are progressed by normal Data/BAR + // processing, and the scoreboard is independent of reassembly state. + auto bufferExpiredFragments = receiveBuffer->removeExpiredFragments(currentTime, maxReceiveLifetime); + expiredFragments.insert(expiredFragments.end(), bufferExpiredFragments.begin(), bufferExpiredFragments.end()); + } + return expiredFragments; +} + std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress originatorAddr) { std::vector frames; @@ -228,7 +261,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists SequenceNumberCyclic earliestSeqNum = SequenceNumberCyclic(0); const auto& buffer = receiveBuffer->getBuffer(); for (auto it : buffer) { - if (isComplete(it.second)) { + if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = earliestFragments.at(0)->peekAtFront()->getSequenceNumber(); break; @@ -238,7 +271,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists for (auto it : buffer) { SequenceNumberCyclic currentSeqNum = it.second.at(0)->peekAtFront()->getSequenceNumber(); if (currentSeqNum < earliestSeqNum) { - if (isComplete(it.second)) { + if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = currentSeqNum; } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 10e27b4444d..c754485d688 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -33,6 +33,7 @@ class INET_API BlockAckReordering protected: std::map, ReceiveBuffer *> receiveBuffers; + simtime_t maxReceiveLifetime = SIMTIME_MAX; protected: static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); @@ -40,16 +41,24 @@ class INET_API BlockAckReordering ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); std::vector getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer); - bool isComplete(const Fragments& fragments); void passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber); void releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer); ReceiveBuffer *createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement); bool addMsduIfComplete(ReceiveBuffer *receiveBuffer, ReorderBuffer& reorderBuffer, SequenceNumberCyclic seqNum); public: + struct QosFrameProcessingResult { + ReorderBuffer frames; + Fragments tombstonedFragments; + }; + + explicit BlockAckReordering(simtime_t maxReceiveLifetime = SIMTIME_MAX) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BlockAckReordering(); std::vector resetReceiveBuffer(Tid tid, MacAddress originatorAddr); + simtime_t getNextExpirationTime() const; + std::vector removeExpiredFragments(simtime_t currentTime); + QosFrameProcessingResult processReceivedQoSFrameWithResult(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 4ab9a0676cb..f3a785bb8d3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h" +#include + namespace inet { namespace ieee80211 { @@ -16,16 +18,55 @@ ReceiveBuffer::ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSe { } +bool ReceiveBuffer::isComplete(const Fragments& fragments) +{ + FragmentNumber terminalFragmentNumber = -1; + bool hasContradictoryTerminalMarkers = false; + std::set fragmentNumbers; + for (auto fragment : fragments) { + if (fragment == nullptr) + continue; + const auto& header = fragment->peekAtFront(); + if (!header->getMoreFragments()) { + if (terminalFragmentNumber == -1) + terminalFragmentNumber = header->getFragmentNumber(); + else if (terminalFragmentNumber != header->getFragmentNumber()) + hasContradictoryTerminalMarkers = true; + } + fragmentNumbers.insert(header->getFragmentNumber()); + } + if (terminalFragmentNumber < 0 || hasContradictoryTerminalMarkers || fragmentNumbers.size() != (size_t)terminalFragmentNumber + 1) + return false; + for (FragmentNumber fragmentNumber = 0; fragmentNumber <= terminalFragmentNumber; fragmentNumber++) + if (fragmentNumbers.find(fragmentNumber) == fragmentNumbers.end()) + return false; + return true; +} + +void ReceiveBuffer::pruneExpiredFragmentSequences() +{ + for (auto it = expiredFragmentSequences.begin(); it != expiredFragmentSequences.end();) { + if (SequenceNumberCyclic(*it) < nextExpectedSequenceNumber) + it = expiredFragmentSequences.erase(it); + else + ++it; + } +} + // // Upon the receipt of a QoS data frame from the originator for which a Block Ack agreement exists, the recipient // buffers the MSDU regardless of the value of the Ack Policy subfield within the QoS Control field of the QoS // data frame, unless the sequence number of the frame is older than the NextExpectedSequenceNumber for that // Block Ack agreement, in which case the frame is discarded because it is either old or a duplicate. // -bool ReceiveBuffer::insertFrame(Packet *dataPacket, const Ptr& dataHeader) +ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader) { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); + bool isFragmented = dataHeader->getMoreFragments() || fragmentNumber != 0; + pruneExpiredFragmentSequences(); + if (isFragmented && expiredFragmentSequences.find(sequenceNumber.get()) != expiredFragmentSequences.end()) + return FrameInsertionResult::REJECTED_EXPIRED; // The total number of MPDUs in these MSDUs may not // exceed the reorder buffer size in the receiver. if (length < bufferSize && nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize) { @@ -36,19 +77,26 @@ bool ReceiveBuffer::insertFrame(Packet *dataPacket, const PtrpeekAtFront(); if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) - return false; + return FrameInsertionResult::REJECTED; } fragments.push_back(dataPacket); } else { buffer[sequenceNumber.get()].push_back(dataPacket); + bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; } // The total number of frames that can be sent depends on the total // number of MPDUs in all the outstanding MSDUs. length++; - return true; + // Once an entry has received a fragmented MPDU, keep that identity + // tied to the generation even if a later malformed or + // unfragmented-shaped header is accepted into the same sequence slot. + auto& bufferEntry = bufferEntries[sequenceNumber.get()]; + bufferEntry.hasFragmentedIdentity |= isFragmented; + bufferEntry.receiveLifetimeActive = bufferEntry.hasFragmentedIdentity && !isComplete(buffer[sequenceNumber.get()]); + return FrameInsertionResult::INSERTED; } - return false; + return FrameInsertionResult::REJECTED; } void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) @@ -59,6 +107,7 @@ void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) length -= it->second.size(); for (auto fragment : it->second) delete fragment; + bufferEntries.erase(it->first); it = buffer.erase(it); } else @@ -71,6 +120,7 @@ void ReceiveBuffer::removeFrame(SequenceNumberCyclic sequenceNumber) auto it = buffer.find(sequenceNumber.get()); if (it != buffer.end()) { length -= it->second.size(); + bufferEntries.erase(sequenceNumber.get()); buffer.erase(sequenceNumber.get()); } else @@ -83,10 +133,55 @@ ReceiveBuffer::Fragments ReceiveBuffer::extractFrames() for (auto& [sequenceNumber, fragments] : buffer) frames.insert(frames.end(), fragments.begin(), fragments.end()); buffer.clear(); + bufferEntries.clear(); + expiredFragmentSequences.clear(); length = 0; return frames; } +simtime_t ReceiveBuffer::getNextExpirationTime(simtime_t maxReceiveLifetime) const +{ + if (maxReceiveLifetime == SIMTIME_MAX) + return SIMTIME_MAX; + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& [sequenceNumber, entry] : bufferEntries) + if (entry.receiveLifetimeActive) + nextExpirationTime = std::min(nextExpirationTime, entry.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} + +ReceiveBuffer::Fragments ReceiveBuffer::removeExpiredFragments(simtime_t currentTime, simtime_t maxReceiveLifetime) +{ + Fragments expiredFragments; + if (maxReceiveLifetime == SIMTIME_MAX) + return expiredFragments; + for (auto it = buffer.begin(); it != buffer.end();) { + auto metadataIt = bufferEntries.find(it->first); + ASSERT(metadataIt != bufferEntries.end()); + auto& entry = metadataIt->second; + if (entry.receiveLifetimeActive && currentTime >= entry.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : it->second) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + length -= it->second.size(); + ASSERT(length >= 0); + expiredFragmentSequences.insert(it->first); + bufferEntries.erase(metadataIt); + it = buffer.erase(it); + } + else + ++it; + } + pruneExpiredFragmentSequences(); + return expiredFragments; +} + +void ReceiveBuffer::setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber) +{ + this->nextExpectedSequenceNumber = nextExpectedSequenceNumber; + pruneExpiredFragmentSequences(); +} + ReceiveBuffer::~ReceiveBuffer() { for (auto fragments : buffer) { diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index 70a28e133c2..9e2349eff21 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -8,6 +8,9 @@ #ifndef __INET_RECEIVEBUFFER_H #define __INET_RECEIVEBUFFER_H +#include +#include + #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" @@ -20,9 +23,23 @@ class INET_API ReceiveBuffer public: typedef std::vector Fragments; typedef std::map ReorderBuffer; + enum class FrameInsertionResult { + INSERTED, + REJECTED, + REJECTED_EXPIRED + }; + static bool isComplete(const Fragments& fragments); protected: + struct BufferEntry { + simtime_t receptionStartTime; + bool hasFragmentedIdentity; + bool receiveLifetimeActive; + }; + ReorderBuffer buffer; + std::map bufferEntries; + std::set expiredFragmentSequences; // For each Block Ack agreement, the recipient maintains a MAC variable NextExpectedSequenceNumber. The // NextExpectedSequenceNumber is initialized to to the value of the Starting Block Ack Starting Sequence // Control field of the ADDBA Request frame of the accepted Block Ack agreement. (IEEE 802.11­-11/0381r0) @@ -30,20 +47,25 @@ class INET_API ReceiveBuffer int length = 0; SequenceNumberCyclic nextExpectedSequenceNumber; + void pruneExpiredFragmentSequences(); + public: ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSequenceNumber); virtual ~ReceiveBuffer(); - bool insertFrame(Packet *dataPacket, const Ptr& dataHeader); + FrameInsertionResult insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader); + bool insertFrame(Packet *dataPacket, const Ptr& dataHeader) { return insertFrameWithResult(dataPacket, dataHeader) == FrameInsertionResult::INSERTED; } void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); Fragments extractFrames(); + simtime_t getNextExpirationTime(simtime_t maxReceiveLifetime) const; + Fragments removeExpiredFragments(simtime_t currentTime, simtime_t maxReceiveLifetime); const ReorderBuffer& getBuffer() { return buffer; } int getLength() { return length; } int getBufferSize() { return bufferSize; } SequenceNumberCyclic getNextExpectedSequenceNumber() { return nextExpectedSequenceNumber; } - void setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber) { this->nextExpectedSequenceNumber = nextExpectedSequenceNumber; } + void setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber); bool isFull() { ASSERT(length <= bufferSize); return length == bufferSize; } }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 571dc3fc226..062d81b4736 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h @@ -50,11 +50,12 @@ class INET_API IReassembly virtual std::vector removeExpiredFragments(simtime_t currentTime) = 0; /** - * Discard fragments from the reassembly buffer. Frames are identified by the transmitter - * address, the TID, and the sequence number range [startSeqNumber, endSeqNumber[. - * Set tid=-1 for non-QoS frames. + * Discard fragments from the reassembly buffer and return the detached + * fragments to the caller. Frames are identified by the transmitter + * address, the TID, and the inclusive sequence number range + * [startSeqNumber, endSeqNumber]. Set tid=-1 for non-QoS frames. */ - virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; + virtual std::vector purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 91e2b939bb7..41a01e36158 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -290,8 +290,9 @@ std::vector BasicReassembly::removeExpiredFragments(simtime_t currentT return expiredFragments; } -void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +std::vector BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) { + std::vector purgedFragments; auto isInSequenceRange = [startSeqNumber, endSeqNumber](int sequenceNumber) { return startSeqNumber <= endSeqNumber ? sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : @@ -301,7 +302,8 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb auto sequenceNumber = getRawSequenceNumber(it->first.extendedSequenceNumber); if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange(sequenceNumber)) { for (auto fragment : it->second.fragments) - delete fragment; + if (fragment != nullptr) + purgedFragments.push_back(fragment); it = fragmentsMap.erase(it); } else @@ -324,6 +326,7 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb else ++it; } + return purgedFragments; } BasicReassembly::~BasicReassembly() diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index 7f9ee6a01b0..e63cb2325a0 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -92,7 +92,7 @@ class INET_API BasicReassembly : public IReassembly, public cObject BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BasicReassembly(); virtual Packet *addFragment(Packet *packet) override; - virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; + virtual std::vector purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; virtual simtime_t getNextExpirationTime() const override; virtual std::vector removeExpiredFragments(simtime_t currentTime) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index 9d2f17424ce..65fd471fcc5 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -18,8 +18,11 @@ Define_Module(RecipientMacDataService); void RecipientMacDataService::initialize() { + simtime_t maxReceiveLifetime = par("maxReceiveLifetime"); + if (maxReceiveLifetime < SIMTIME_ZERO) + throw cRuntimeError("maxReceiveLifetime must not be negative"); duplicateRemoval = new LegacyDuplicateRemoval(); - basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + basicReassembly = new BasicReassembly(maxReceiveLifetime); reassemblyTimer = new cMessage("reassemblyTimer"); } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned index 3d6ae2e5e37..1b838483d49 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned @@ -19,7 +19,7 @@ module RecipientMacDataService extends Module { parameters: @class(RecipientMacDataService); - double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs + double maxReceiveLifetime @unit(s) = default(524288us); // Nonnegative relative receive lifetime; IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 2bec4629c43..f378e533d63 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -23,23 +23,26 @@ Define_Module(RecipientQosMacDataService); // TODO refactor to avoid code duplication void RecipientQosMacDataService::initialize() { + maxReceiveLifetime = par("maxReceiveLifetime"); + if (maxReceiveLifetime < SIMTIME_ZERO) + throw cRuntimeError("maxReceiveLifetime must not be negative"); duplicateRemoval = new QoSDuplicateRemoval(); - basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + basicReassembly = new BasicReassembly(maxReceiveLifetime); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); - blockAckReordering = new BlockAckReordering(); - reassemblyTimer = new cMessage("reassemblyTimer"); + blockAckReordering = new BlockAckReordering(maxReceiveLifetime); + receiveLifetimeTimer = new cMessage("receiveLifetimeTimer"); } void RecipientQosMacDataService::handleMessage(cMessage *message) { - if (message != reassemblyTimer) + if (message != receiveLifetimeTimer) throw cRuntimeError("Unknown message"); - expireReassemblyFragments(); - scheduleReassemblyTimer(); + expireReceiveLifetime(); + scheduleReceiveLifetimeTimer(); } -void RecipientQosMacDataService::expireReassemblyFragments() +void RecipientQosMacDataService::expireReceiveLifetime() { for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { PacketDropDetails details; @@ -47,15 +50,21 @@ void RecipientQosMacDataService::expireReassemblyFragments() emit(packetDroppedSignal, packet, &details); delete packet; } + for (auto packet : blockAckReordering->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } } -void RecipientQosMacDataService::scheduleReassemblyTimer() +void RecipientQosMacDataService::scheduleReceiveLifetimeTimer() { - if (reassemblyTimer->isScheduled()) - cancelEvent(reassemblyTimer); - auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (receiveLifetimeTimer->isScheduled()) + cancelEvent(receiveLifetimeTimer); + auto nextExpirationTime = std::min(basicReassembly->getNextExpirationTime(), blockAckReordering->getNextExpirationTime()); if (nextExpirationTime != SIMTIME_MAX) - scheduleAt(nextExpirationTime, reassemblyTimer); + scheduleAt(nextExpirationTime, receiveLifetimeTimer); } void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) @@ -71,11 +80,21 @@ void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress ori delete packet; } } + if (basicReassembly) { + auto droppedFragments = basicReassembly->purge(originatorAddr, tid, 0, 4095); + for (auto packet : droppedFragments) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + scheduleReceiveLifetimeTimer(); } Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { - expireReassemblyFragments(); + expireReceiveLifetime(); Packet *defragmentedPacket = nullptr; for (auto fragment : completeFragments) { auto packet = basicReassembly->addFragment(fragment); @@ -84,7 +103,7 @@ Packet *RecipientQosMacDataService::defragment(std::vector completeFra break; } } - scheduleReassemblyTimer(); + scheduleReceiveLifetimeTimer(); if (defragmentedPacket != nullptr) emit(packetDefragmentedSignal, defragmentedPacket); return defragmentedPacket; @@ -92,9 +111,9 @@ Packet *RecipientQosMacDataService::defragment(std::vector completeFra Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { - expireReassemblyFragments(); + expireReceiveLifetime(); auto packet = basicReassembly->addFragment(mgmtFragment); - scheduleReassemblyTimer(); + scheduleReceiveLifetimeTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -107,6 +126,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data { Enter_Method("dataFrameReceived"); take(dataPacket); + expireReceiveLifetime(); // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; @@ -114,6 +134,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data details.setReason(DUPLICATE_DETECTED); emit(packetDroppedSignal, dataPacket, &details); delete dataPacket; + scheduleReceiveLifetimeTimer(); return std::vector(); } BlockAckReordering::ReorderBuffer frames; @@ -122,8 +143,16 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + if (agreement) { + auto processingResult = blockAckReordering->processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); + frames = processingResult.frames; + for (auto packet : processingResult.tombstonedFragments) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -143,6 +172,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data else ; // TODO drop? } } + scheduleReceiveLifetimeTimer(); std::vector deaggregatedFrames; if (aMsduDeaggregation) { for (auto defragmentedFrame : defragmentedFrames) { @@ -166,16 +196,21 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS { Enter_Method("managementFrameReceived"); take(mgmtPacket); + expireReceiveLifetime(); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { delete mgmtPacket; // A duplicate fragment is acknowledged by HCF but is not a complete // MMPDU. Preserve the existing subtype-specific handling only for an // unfragmented duplicate management frame. - if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) + if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) { + scheduleReceiveLifetimeTimer(); return { {}, mgmtHeader, true }; - else + } + else { + scheduleReceiveLifetimeTimer(); return { {}, nullptr, true }; + } } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); @@ -186,15 +221,19 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting if (dynamicPtrCast(completeHeader)) { delete mgmtPacket; + scheduleReceiveLifetimeTimer(); return { {}, completeHeader, false }; } - else + else { + scheduleReceiveLifetimeTimer(); return { { mgmtPacket }, completeHeader, false }; + } } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); + expireReceiveLifetime(); if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReorderBuffer frames; if (blockAckReordering) { @@ -203,8 +242,10 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); if (agreement) frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); - else + else { + scheduleReceiveLifetimeTimer(); return std::vector(); + } } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -239,14 +280,16 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c } } // TODO MSDU Integrity, Replay Detection, RX MSDU Rate Limiting + scheduleReceiveLifetimeTimer(); return deaggregatedFrames; } + scheduleReceiveLifetimeTimer(); return std::vector(); } RecipientQosMacDataService::~RecipientQosMacDataService() { - cancelAndDelete(reassemblyTimer); + cancelAndDelete(receiveLifetimeTimer); delete duplicateRemoval; delete basicReassembly; delete aMsduDeaggregation; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index 644bba53f1a..b2c7a8a03b3 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,7 +27,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; - cMessage *reassemblyTimer = nullptr; + cMessage *receiveLifetimeTimer = nullptr; + simtime_t maxReceiveLifetime = SIMTIME_MAX; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -44,8 +45,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, virtual void initialize() override; virtual void handleMessage(cMessage *message) override; - virtual void expireReassemblyFragments(); - virtual void scheduleReassemblyTimer(); + virtual void expireReceiveLifetime(); + virtual void scheduleReceiveLifetimeTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index b1ba0021f7e..f6fd0bf7316 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned @@ -22,7 +22,7 @@ module RecipientQosMacDataService extends Module { parameters: @class(RecipientQosMacDataService); - double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs + double maxReceiveLifetime @unit(s) = default(524288us); // Nonnegative relative receive lifetime; IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index fcf1f39d75e..afa5323feb4 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -7,7 +7,7 @@ # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, ca86-69c6/tplx;521b-977a/~tNl;f7ee-50fd/~tND;5471-336a/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, f8a7-3cf6/tplx;b738-d1b7/~tNl;47b1-07f1/~tND;0635-98e5/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, e8ad-ca0f/tplx;b738-d1b7/~tNl;9161-07f7/~tND;6e70-47f6/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 9c66-e6f0/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 85bd-85c6/tplx;22f6-085d/~tNl;e4ea-962c/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 5e5e3ce3e3e..296c7cd8510 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -38,6 +38,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -185,6 +186,8 @@ class TestRecipientHandler : public RecipientBlockAckAgreementHandler class TestBlockAckReordering : public BlockAckReordering { public: + using BlockAckReordering::BlockAckReordering; + int getNumReceiveBuffers() const { return receiveBuffers.size(); } ReceiveBuffer *getReceiveBuffer(Tid tid, MacAddress originatorAddress) const { auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); @@ -1014,6 +1017,30 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const return check_and_cast(module); }; +// maxReceiveLifetime is a relative duration and must be nonnegative before +// either recipient allocates a receive-lifetime consumer. Zero remains valid. +auto assertMaxReceiveLifetimeValidation = [this](const char *typeName, const char *namePrefix) { + auto invalidModule = cModuleType::get(typeName)->create((std::string(namePrefix) + "Negative").c_str(), this); + invalidModule->par("maxReceiveLifetime").setDoubleValue(-1e-6); + bool threw = false; + try { + invalidModule->callInitialize(); + } + catch (cRuntimeError&) { + threw = true; + } + ASSERT(threw); + invalidModule->deleteModule(); + + auto zeroModule = cModuleType::get(typeName)->create((std::string(namePrefix) + "Zero").c_str(), this); + zeroModule->par("maxReceiveLifetime").setDoubleValue(0); + zeroModule->callInitialize(); + zeroModule->callFinish(); + zeroModule->deleteModule(); +}; +assertMaxReceiveLifetimeValidation("inet.linklayer.ieee80211.mac.recipient.RecipientMacDataService", "recipientMacDataServiceLifetime"); +assertMaxReceiveLifetimeValidation("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService", "recipientQosMacDataServiceLifetime"); + { TestOriginatorHandler tokenHandler; tokenHandler.setNextDialogToken(255); @@ -2478,6 +2505,174 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } +// Receive-buffer lifetime starts when the first fragment of an incomplete +// fragmented MPDU is retained. Later fragments do not refresh that deadline; +// complete MPDUs held only for ordering remain available until normal window +// processing releases them. +{ + auto makeReorderFragment = [&](const char *name, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto lifetime = SimTime(1, SIMTIME_US); + RecipientBlockAckAgreement agreement(peer1, 6, SequenceNumberCyclic(100), 64, 0); + TestBlockAckReordering reordering(lifetime); + auto firstFragment = makeReorderFragment("receiveLifetimeFirstFragment", SequenceNumberCyclic(100), 1, true, 0x61); + auto firstReceptionTime = simTime(); + ASSERT(reordering.processReceivedQoSFrame(&agreement, firstFragment, firstFragment->peekAtFront()).empty()); + auto firstDeadline = reordering.getNextExpirationTime(); + ASSERT(firstDeadline == firstReceptionTime + lifetime); + wait(0.5e-6); + auto laterFragment = makeReorderFragment("receiveLifetimeLaterFragment", SequenceNumberCyclic(100), 2, false, 0x62); + ASSERT(reordering.processReceivedQoSFrame(&agreement, laterFragment, laterFragment->peekAtFront()).empty()); + ASSERT(reordering.getNextExpirationTime() == firstDeadline); + + auto completeEntry = makeReorderFragment("receiveLifetimeCompleteEntry", SequenceNumberCyclic(101), 0, false, 0x63); + ASSERT(reordering.processReceivedQoSFrame(&agreement, completeEntry, completeEntry->peekAtFront()).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(6, peer1); + ASSERT(receiveBuffer->getLength() == 3); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(100)); + auto expiredFragments = reordering.removeExpiredFragments(firstDeadline); + ASSERT(expiredFragments.size() == 2); + ASSERT(receiveBuffer->getLength() == 1); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(100)); + ASSERT(receiveBuffer->getBuffer().find(101) != receiveBuffer->getBuffer().end()); + for (auto packet : expiredFragments) + delete packet; + + auto lateFragment = makeReorderFragment("receiveLifetimeLateFragment", SequenceNumberCyclic(100), 0, true, 0x64); + ASSERT(reordering.processReceivedQoSFrame(&agreement, lateFragment, lateFragment->peekAtFront()).empty()); + ASSERT(receiveBuffer->getLength() == 1); + auto retainedCompleteEntry = reordering.resetReceiveBuffer(6, peer1); + ASSERT(retainedCompleteEntry.size() == 1); + delete retainedCompleteEntry.front(); + + // The tombstone is retired by normal modulo-window progress, so a later + // complete wrap can reuse the raw sequence number without permanent loss. + RecipientBlockAckAgreement wrapAgreement(peer1, 6, SequenceNumberCyclic(4094), 64, 0); + TestBlockAckReordering wrapReordering(lifetime); + auto wrapFragment = makeReorderFragment("receiveLifetimeWrapFragment", SequenceNumberCyclic(4095), 1, false, 0x65); + ASSERT(wrapReordering.processReceivedQoSFrame(&wrapAgreement, wrapFragment, wrapFragment->peekAtFront()).empty()); + auto wrapDeadline = wrapReordering.getNextExpirationTime(); + auto expiredWrapFragments = wrapReordering.removeExpiredFragments(wrapDeadline); + ASSERT(expiredWrapFragments.size() == 1); + delete expiredWrapFragments.front(); + auto wrapReceiveBuffer = wrapReordering.getReceiveBuffer(6, peer1); + wrapReceiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(0)); + for (int sequenceNumber = 1; sequenceNumber <= 4095; sequenceNumber++) + wrapReceiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto reusedWrapFragment = makeReorderFragment("receiveLifetimeReusedWrapFragment", SequenceNumberCyclic(4095), 0, true, 0x66); + ASSERT(wrapReordering.processReceivedQoSFrame(&wrapAgreement, reusedWrapFragment, reusedWrapFragment->peekAtFront()).empty()); + auto reusedWrapFragments = wrapReordering.resetReceiveBuffer(6, peer1); + ASSERT(reusedWrapFragments.size() == 1); + delete reusedWrapFragments.front(); +} + +// Different terminal markers for one sequence cannot be interpreted as a +// complete MPDU. Keep the entry incomplete and expiring instead of releasing +// a hybrid or clearing its receive-lifetime deadline. +{ + auto makeContradictoryFragment = [&](const char *name, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(250)); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto lifetime = SimTime(1, SIMTIME_US); + RecipientBlockAckAgreement agreement(peer1, 6, SequenceNumberCyclic(250), 64, 0); + TestBlockAckReordering reordering(lifetime); + auto finalFragment = makeContradictoryFragment("contradictoryFinalFragment", 1, false, 0x67); + auto receptionTime = simTime(); + ASSERT(reordering.processReceivedQoSFrame(&agreement, finalFragment, finalFragment->peekAtFront()).empty()); + auto fragmentZero = makeContradictoryFragment("contradictoryFragmentZero", 0, false, 0x68); + ASSERT(reordering.processReceivedQoSFrame(&agreement, fragmentZero, fragmentZero->peekAtFront()).empty()); + ASSERT(reordering.getNextExpirationTime() == receptionTime + lifetime); + auto receiveBuffer = reordering.getReceiveBuffer(6, peer1); + ASSERT(receiveBuffer->getLength() == 2); + auto expiredFragments = reordering.removeExpiredFragments(receptionTime + lifetime); + ASSERT(expiredFragments.size() == 2); + for (auto packet : expiredFragments) + delete packet; + ASSERT(receiveBuffer->getLength() == 0); +} + +// A retained reorder-buffer fragment arms the recipient's common lifetime +// timer. Reorder expiry and BasicReassembly expiry are staggered, and every +// detached packet is reported exactly once as OTHER_PACKET_DROP. +{ + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("reorderLifetimeDataService", this); + auto lifetime = SimTime(2, SIMTIME_US); + serviceModule->par("maxReceiveLifetime").setDoubleValue(lifetime.dbl()); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + TestPacketDropSignalListener dropListener; + service->subscribe(packetDroppedSignal, &dropListener); + + RecipientBlockAckAgreementHandler agreementHandler; + TestRecipientPolicy policy; + TestCallback callback; + auto request = makeRequest(peer1, 6, 92, SequenceNumberCyclic(300), 64); + auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); + ASSERT(agreement != nullptr); + + auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71); + ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); + auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72); + ASSERT(service->dataFrameReceived(reorderFinalFragment, reorderFinalFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(dropListener.numSignals == 0); + wait(0.5e-6); + + // No agreement for peer3: this fragment is retained by BasicReassembly, + // with a later deadline than the already retained reorder fragments. + auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73); + ASSERT(service->dataFrameReceived(basicFragment, basicFragment->peekAtFront(), &agreementHandler).empty()); + wait(1.6e-6); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); + + // The expired sequence remains tombstoned until the reorder window moves + // past it. A late fragment is rejected and reported once by the service, + // while no hybrid or partial frame is delivered. + auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74); + ASSERT(service->dataFrameReceived(lateReorderFragment, lateReorderFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(dropListener.numSignals == 3); + ASSERT(dropListener.numOtherPacketDrops == 3); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(300), 0)); + wait(0.3e-6); + ASSERT(dropListener.numSignals == 3); + wait(0.2e-6); + ASSERT(dropListener.numSignals == 4); + ASSERT(dropListener.numOtherPacketDrops == 4); + service->unsubscribe(packetDroppedSignal, &dropListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); +} + // The recipient data service owns reorder-buffer reset observability: every // buffered MPDU is reported exactly once before it is deleted. { @@ -2491,11 +2686,46 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; auto bufferedPacket = new Packet("signaledOldWindowPacket", bufferedHeader); auto signaledOldWindowFrames = recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, &immediateRecipientHandler); ASSERT(signaledOldWindowFrames.empty()); + + auto makeBasicFragment = [&](const char *name, MacAddress transmitterAddress, Tid tid, int sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer1, tid, SequenceNumberCyclic(sequenceNumber)); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto targetFirst = makeBasicFragment("signaledTargetBasicFragment", peer2, 7, 20, 0, true, 0x81); + ASSERT(recipientDataService->dataFrameReceived(targetFirst, targetFirst->peekAtFront(), &immediateRecipientHandler).empty()); + auto samePeerDifferentTidFirst = makeBasicFragment("signaledDifferentTidBasicFragment", peer2, 8, 21, 0, true, 0x82); + ASSERT(recipientDataService->dataFrameReceived(samePeerDifferentTidFirst, samePeerDifferentTidFirst->peekAtFront(), &immediateRecipientHandler).empty()); + auto differentPeerSameTidFirst = makeBasicFragment("signaledDifferentPeerBasicFragment", peer3, 7, 22, 0, true, 0x83); + ASSERT(recipientDataService->dataFrameReceived(differentPeerSameTidFirst, differentPeerSameTidFirst->peekAtFront(), &immediateRecipientHandler).empty()); + recipientDataService->resetBlockAckReordering(6, peer1); ASSERT(dropListener.numSignals == 1); ASSERT(dropListener.numOtherPacketDrops == 1); + // Resetting one originator/TID agreement also purges its BasicReassembly + // state, while retaining fragments for a different TID or peer. + recipientDataService->resetBlockAckReordering(7, peer2); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); recipientDataService->resetBlockAckReordering(6, peer1); - ASSERT(dropListener.numSignals == 1); + recipientDataService->resetBlockAckReordering(7, peer2); + ASSERT(dropListener.numSignals == 2); + auto samePeerDifferentTidFinal = makeBasicFragment("signaledDifferentTidBasicFinal", peer2, 8, 21, 1, false, 0x92); + auto samePeerDifferentTidFrames = recipientDataService->dataFrameReceived(samePeerDifferentTidFinal, + samePeerDifferentTidFinal->peekAtFront(), &immediateRecipientHandler); + ASSERT(samePeerDifferentTidFrames.size() == 1); + delete samePeerDifferentTidFrames.front(); + auto differentPeerSameTidFinal = makeBasicFragment("signaledDifferentPeerBasicFinal", peer3, 7, 22, 1, false, 0x93); + auto differentPeerSameTidFrames = recipientDataService->dataFrameReceived(differentPeerSameTidFinal, + differentPeerSameTidFinal->peekAtFront(), &immediateRecipientHandler); + ASSERT(differentPeerSameTidFrames.size() == 1); + delete differentPeerSameTidFrames.front(); recipientDataService->unsubscribe(packetDroppedSignal, &dropListener); recipientDataServiceModule->callFinish(); recipientDataServiceModule->deleteModule(); @@ -5049,6 +5279,16 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { ASSERT(completed->peekAt(completedResponse->getChunkLength(), B(newPayload.size()))->getBytes() == newPayload); delete completed; delete recoveredFragments; + + // purge() remains the explicit agreement/lifecycle boundary and returns + // detached fragments to its caller for observable drop handling. + auto purgeFragments = makeResponseFragments("reassemblyPurgedResponse", 31, 41, { 0xb0, 0xb1, 0xb2 }, 4094, 2); + ASSERT(reassembly.addFragment(purgeFragments->at(0)) == nullptr); + auto purgedFragments = reassembly.purge(peer1, -1, 4094, 4094); + ASSERT(purgedFragments.size() == 1); + delete purgedFragments.front(); + delete purgeFragments->at(1); + delete purgeFragments; } // A terminal duplicate on an occupied nonterminal slot is still observed for From 6ffc197118f82b834423537f3890b75286264795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:48:19 +0200 Subject: [PATCH 37/91] ieee80211: add+fix: quarantine expired Block Ack agreements Retained teardown state is not an active data-plane agreement. Quarantine expired agreements, release their outstanding Block Ack frames, and relookup the original generation after the release callback before generating DELBA. Extend the expiry regression to replacement during frame release. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.add+change.fix | test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 24 +- doc/src/migration-guide/index.rst | 11 + .../OriginatorBlockAckAgreementHandler.cc | 28 +- .../OriginatorBlockAckAgreementHandler.h | 3 +- .../RecipientBlockAckAgreementHandler.cc | 19 +- .../RecipientBlockAckAgreementHandler.h | 3 +- .../blockack/RecipientBlockAckProcedure.cc | 2 +- .../IBlockAckAgreementHandlerCallback.h | 6 + .../IOriginatorBlockAckAgreementHandler.h | 6 +- .../mac/contract/IOriginatorQoSAckPolicy.h | 6 +- .../IRecipientBlockAckAgreementHandler.h | 6 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 38 ++- .../ieee80211/mac/coordinationfunction/Hcf.h | 2 + .../framesequence/PrimitiveFrameSequences.cc | 2 +- .../ieee80211/mac/framesequence/TxOpFs.cc | 4 +- .../mac/originator/OriginatorQosAckPolicy.cc | 53 +-- .../mac/originator/OriginatorQosAckPolicy.h | 6 +- .../ieee80211/mac/originator/QosAckHandler.cc | 16 + .../ieee80211/mac/originator/QosAckHandler.h | 4 + .../mac/recipient/RecipientQosAckPolicy.cc | 3 +- .../recipient/RecipientQosMacDataService.cc | 19 +- tests/unit/Ieee80211AddbaTransaction_1.test | 309 +++++++++++++++++- .../Ieee80211OriginatorExpiryReentrant_1.test | 15 +- .../Ieee80211RecipientExpiryReentrant_1.test | 13 +- 24 files changed, 519 insertions(+), 79 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index c58fffc7094..c49dafcff89 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -335,13 +335,16 @@ Notable backward incompatible changes are the following: 5. Block Ack DELBA agreement ownership - Custom originator agreement handlers must explicitly implement - isDelbaPending(). - Custom IFrameSequenceHandler implementations must explicitly implement - cancelFrameSequence() and abortFrameSequence(). - Custom agreement callbacks must implement cancelBlockAckTeardown(); - recipient handlers must implement processAcknowledgedDelba(), - processAbortedDelba(), getPendingTeardownGenerationId() and isDelbaPending(). + Custom IBlockAckAgreementHandlerCallback implementations must explicitly + implement releaseBlockAckAgreementFrames() and cancelBlockAckTeardown(). + Custom IFrameSequenceHandler implementations must implement both + cancelFrameSequence() and abortFrameSequence(), preserving their deferred + and immediate cancellation contracts. Custom originator and recipient + agreement handlers must implement isDelbaPending(); recipient handlers must + also implement processAcknowledgedDelba(), processAbortedDelba() and + getPendingTeardownGenerationId(). These methods no longer supply default + behavior in the interfaces. The built-in implementations already provide + these operations. IOriginatorBlockAckAgreementHandler::processReceivedDelba(), IOriginatorBlockAckAgreementHandler::processTransmittedDelba(), and @@ -412,6 +415,13 @@ Notable backward incompatible changes are the following: Block Ack reordering. A later stage no longer gives an old incomplete body a fresh lifetime, so delayed or incomplete bodies may be dropped earlier. +11. Expired Agreement Quarantine and Frame Release + + Expired Block Ack agreements are quarantined from data-plane use while teardown state + is retained. Outstanding frames are released through the normal + acknowledgment/recovery paths before teardown proceeds, including when callbacks + replace agreement state. + 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 958e7845cb6..97892e472d6 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -178,6 +178,17 @@ owned packets; callers must report their final drop and delete them. Use and tombstoned fragments. Negative ``maxReceiveLifetime`` values are rejected; zero is a valid immediate-expiry setting. +Expired Agreement Quarantine and Frame Release +---------------------------------------------- + +Custom ``IBlockAckAgreementHandlerCallback`` implementations must explicitly implement +``releaseBlockAckAgreementFrames()`` and report whether they changed outstanding state. +Retained teardown state is not an active data-plane agreement; use the active-agreement +query for data transmission, acknowledgment and receive admission. Frame release may +synchronously remove or replace an agreement, so relookup the original generation before +generating its DELBA. If an implementation has no outstanding frames, returning false +must be an explicit implementation decision. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 9245bf55fe7..c830853cc34 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -98,13 +98,14 @@ void OriginatorBlockAckAgreementHandler::addbaResponseTimeoutExpired(IOriginator scheduleAddbaResponseTimer(callback); } -void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +bool OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in IEEE 802.11-2024, Figure 11-34. simtime_t now = simTime(); + bool expired = false; // Queue callbacks can erase the current agreement, a sibling, or replace // either generation. Keep only value identities across outward calls. std::vector, uint64_t>> expiredAgreements; @@ -122,12 +123,19 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall !agreement->getIsAddbaResponseReceived() || agreement->isInactivityExpired() || agreement->getExpirationTime() > now) continue; agreement->markInactivityExpired(); + expired = true; + if (agreementHandlerCallback != nullptr) + agreementHandlerCallback->releaseBlockAckAgreementFrames(receiverAddr, tid); + agreement = getAgreement(receiverAddr, tid); + if (agreement == nullptr || agreement->getTransactionId() != generationId || !agreement->isInactivityExpired()) + continue; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); delbaPacket->addTag()->setGenerationId(generationId); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT } scheduleInactivityTimer(agreementHandlerCallback); + return expired; } const Ptr OriginatorBlockAckAgreementHandler::buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) @@ -154,8 +162,8 @@ const Ptr OriginatorBlockAckAgreementHandler::buildAddbaR void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) { if (auto basicBlockAck = dynamicPtrCast(blockAck)) { - auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); - if (agreement && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); + if (agreement != nullptr) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -184,6 +192,12 @@ OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(Ma return it != blockAckAgreements.end() ? it->second : nullptr; } +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getActiveAgreement(MacAddress receiverAddr, Tid tid) +{ + auto agreement = getAgreement(receiverAddr, tid); + return agreement != nullptr && agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() ? agreement : nullptr; +} + bool OriginatorBlockAckAgreementHandler::isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const { auto it = blockAckAgreements.find(std::make_pair(receiverAddr, tid)); @@ -373,6 +387,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: return nullptr; bool cancelPendingTransaction = agreement->isPending(); std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; scheduleAddbaResponseTimer(callback); @@ -385,6 +401,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) @@ -430,6 +448,8 @@ OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::proce OriginatorBlockAckAgreementAbortResult result; result.handled = true; result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (result.terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); if (callback != nullptr) callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); @@ -451,6 +471,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: if (pendingTeardownIt != pendingTeardownTransactionIds.end()) pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getTransmitterAddress(), delba->getTid()); scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 891c01432c9..5fa2c7b5d3a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -54,10 +54,11 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) override; + virtual OriginatorBlockAckAgreement *getActiveAgreement(MacAddress receiverAddr, Tid tid) override; virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const override; virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const override; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 4516318e9b3..35c96fbfac3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -41,8 +41,8 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const PtrgetAckPolicy() == AckPolicy::BLOCK_ACK) { // TODO + Implicit Block Ack Tid tid = qosHeader->getTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); - auto agreement = getAgreement(tid, originatorAddr); - if (agreement && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(tid, originatorAddr); + if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } @@ -53,20 +53,21 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) { - auto agreement = getAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); - if (agreement != nullptr && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } } -void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +bool RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in IEEE 802.11-2024, Figure 11-34. simtime_t now = simTime(); + bool expired = false; // Queue callbacks can erase the current agreement, a sibling, or replace // either generation. Keep only value identities across outward calls. std::vector, uint64_t>> expiredAgreements; @@ -84,12 +85,14 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb agreement->isInactivityExpired() || agreement->getExpirationTime() > now) continue; agreement->markInactivityExpired(); + expired = true; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); delbaPacket->addTag()->setGenerationId(generationId); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT } scheduleInactivityTimer(agreementHandlerCallback); + return expired; } // @@ -152,6 +155,12 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getActiveAgreement(Tid tid, MacAddress originatorAddr) +{ + auto agreement = getAgreement(tid, originatorAddr); + return agreement != nullptr && !agreement->isInactivityExpired() ? agreement : nullptr; +} + RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 4d979ef547e..8792f070d7d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -50,9 +50,10 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; + virtual RecipientBlockAckAgreement *getActiveAgreement(Tid tid, MacAddress originatorAddr) override; virtual uint64_t getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const override; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index ab7cb050e5f..2c94d1cac47 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -21,7 +21,7 @@ void RecipientBlockAckProcedure::processReceivedBlockAckReq(Packet *blockAckPack { numReceivedBlockAckReq++; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { - auto agreement = blockAckAgreementHandler->getAgreement(basicBlockAckReq->getTidInfo(), basicBlockAckReq->getTransmitterAddress()); + auto agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(basicBlockAckReq->getTidInfo(), basicBlockAckReq->getTransmitterAddress()); if (ackPolicy->isBlockAckNeeded(basicBlockAckReq, agreement)) { auto blockAck = buildBlockAck(basicBlockAckReq, agreement); auto duration = ackPolicy->computeBasicBlockAckDurationField(blockAckPacketReq, basicBlockAckReq); diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index 7de2599b89f..b3f4e5a97fe 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -34,6 +34,12 @@ class INET_API IBlockAckAgreementHandlerCallback virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; + // Marks originator data frames that were waiting for this agreement's + // Block Ack exchange as eligible for retry with an alternative + // acknowledgment policy. This is needed whenever an agreement becomes + // unavailable, including before its retained teardown object is removed. + // The return value reports whether any ACK state changed. + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) = 0; // Removes queued siblings of a sender-local DELBA without assuming that // the frame is still removable from the active frame sequence. The // agreement owner remains responsible for rejecting stale packets. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 02000861822..b6f3ba38e0f 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -58,10 +58,14 @@ class INET_API IOriginatorBlockAckAgreementHandler // transaction and sibling packets were cancelled through the callback. virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) = 0; + // Returns the installed agreement only while it can be used by the data + // plane. Lifecycle code must use getAgreement() to retain generation-safe + // teardown state after inactivity expiry. + virtual OriginatorBlockAckAgreement *getActiveAgreement(MacAddress receiverAddr, Tid tid) = 0; virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const = 0; virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const = 0; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 8f03c2fd052..4cfc2e63c24 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -16,6 +16,8 @@ namespace inet { namespace ieee80211 { +class IOriginatorBlockAckAgreementHandler; + class INET_API IOriginatorQoSAckPolicy { public: @@ -23,9 +25,9 @@ class INET_API IOriginatorQoSAckPolicy virtual bool isAckNeeded(const Ptr& header) const = 0; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const = 0; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const = 0; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const = 0; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index d853ece3f21..6841c5b17b0 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -45,9 +45,13 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; + // Returns the installed agreement only while it can be used by the data + // plane. Lifecycle code must use getAgreement() to retain generation-safe + // teardown state after inactivity expiry. + virtual RecipientBlockAckAgreement *getActiveAgreement(Tid tid, MacAddress originatorAddr) = 0; virtual uint64_t getPendingTeardownGenerationId(Tid, MacAddress) const = 0; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 8d19b3fb81c..98684e331f9 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -336,8 +336,14 @@ void Hcf::handleMessage(cMessage *msg) } else if (msg == inactivityTimer) { if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) { - originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); - recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + blockAckInactivityExpiryInProgress = true; + bool changed = originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + changed |= recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + blockAckInactivityExpiryInProgress = false; + if (changed) { + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } } else throw cRuntimeError("Unknown event"); @@ -390,7 +396,7 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (hasFrameToTransmit(ac)) { + if (!blockAckInactivityExpiryInProgress && hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -477,6 +483,20 @@ void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) rescheduleAt(deadline, addbaResponseTimer); } +bool Hcf::releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) +{ + Enter_Method("releaseBlockAckAgreementFrames"); + if (edca == nullptr) + return false; + bool changed = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + if (edcaf != nullptr && edcaf->getAckHandler() != nullptr) + changed |= edcaf->getAckHandler()->releaseBlockAckAgreementFrames(peerAddress, tid); + } + return changed; +} + void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) { Enter_Method("cancelAddbaTransaction"); @@ -991,6 +1011,9 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); + if (dataHeader->getAckPolicy() == BLOCK_ACK && + (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()) == nullptr)) + edcaf->getAckHandler()->releaseBlockAckAgreementFrames(dataHeader->getReceiverAddress(), dataHeader->getTid()); if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } @@ -1217,10 +1240,13 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { EV_INFO << "BasicBlockAck has arrived" << std::endl; + if (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo()) == nullptr) { + EV_INFO << "Ignoring BasicBlockAck without an active Block Ack agreement.\n"; + return; + } edcaf->getRecoveryProcedure()->blockAckFrameReceived(); auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); + originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); EV_TRACE << "It has acknowledged the following frames:" << std::endl; for (auto it : ackedSeqAndFragNums) EV_TRACE << " sequenceNumber = " << it.second.second.getSequenceNumber() << ", fragmentNumber = " << (int)it.second.second.getFragmentNumber() << std::endl; @@ -1293,7 +1319,7 @@ void Hcf::transmitFrame(Packet *packet, simtime_t ifs) if (auto dataFrame = dynamicPtrCast(header)) { OriginatorBlockAckAgreement *agreement = nullptr; if (originatorBlockAckAgreementHandler) - agreement = originatorBlockAckAgreementHandler->getAgreement(dataFrame->getReceiverAddress(), dataFrame->getTid()); + agreement = originatorBlockAckAgreementHandler->getActiveAgreement(dataFrame->getReceiverAddress(), dataFrame->getTid()); auto ackPolicy = originatorAckPolicy->computeAckPolicy(packet, dataFrame, agreement); auto dataHeader = packet->removeAtFront(); dataHeader->setAckPolicy(ackPolicy); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 5f0ab335097..4faa36a39d8 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -70,6 +70,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // their role so one role cannot cancel the other's timeout. simtime_t originatorInactivityDeadline = SIMTIME_MAX; simtime_t recipientInactivityDeadline = SIMTIME_MAX; + bool blockAckInactivityExpiryInProgress = false; // Transmission and Reception IRx *rx = nullptr; @@ -204,6 +205,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) override; virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 9ad68ed2969..2980f92720c 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -392,7 +392,7 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con { switch (step) { case 0: { - auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure); + auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure, context->getQoSContext()->blockAckAgreementHandler); auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc index 302ab7c32af..500769b5938 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc @@ -47,7 +47,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte { auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); - if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure)) + if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure, context->getQoSContext()->blockAckAgreementHandler)) return 2; if (dynamicPtrCast(macHeader)) return 3; @@ -55,7 +55,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte auto dataHeaderToTransmit = dynamicPtrCast(macHeader); OriginatorBlockAckAgreement *agreement = nullptr; if (context->getQoSContext()->blockAckAgreementHandler) - agreement = context->getQoSContext()->blockAckAgreementHandler->getAgreement(dataHeaderToTransmit->getReceiverAddress(), dataHeaderToTransmit->getTid()); + agreement = context->getQoSContext()->blockAckAgreementHandler->getActiveAgreement(dataHeaderToTransmit->getReceiverAddress(), dataHeaderToTransmit->getTid()); auto ackPolicy = context->getQoSContext()->ackPolicy->computeAckPolicy(frameToTransmit, dataHeaderToTransmit, agreement); if (ackPolicy == AckPolicy::BLOCK_ACK) return 0; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 206d9e8b73d..7e6de21f2f3 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" + namespace inet { namespace ieee80211 { @@ -31,13 +33,20 @@ bool OriginatorQosAckPolicy::isAckNeeded(const Ptr& h return !header->getReceiverAddress().isMulticast(); } -std::map> OriginatorQosAckPolicy::getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const +std::map, std::vector> OriginatorQosAckPolicy::getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { auto outstandingFrames = inProgressFrames->getOutstandingFrames(); - std::map> outstandingFramesPerReceiver; - for (auto frame : outstandingFrames) - outstandingFramesPerReceiver[frame->peekAtFront()->getReceiverAddress()].push_back(frame); - return outstandingFramesPerReceiver; + std::map, std::vector> outstandingFramesPerAgreement; + if (blockAckAgreementHandler == nullptr) + return outstandingFramesPerAgreement; + for (auto frame : outstandingFrames) { + auto dataHeader = frame->peekAtFront(); + auto receiverAddress = dataHeader->getReceiverAddress(); + auto tid = dataHeader->getTid(); + if (blockAckAgreementHandler->getActiveAgreement(receiverAddress, tid) != nullptr) + outstandingFramesPerAgreement[std::make_pair(receiverAddress, tid)].push_back(frame); + } + return outstandingFramesPerAgreement; } SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const std::vector& outstandingFrames) const @@ -64,10 +73,10 @@ bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector } // FIXME -bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const +bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { + auto outstandingFramesPerAgreement = getOutstandingFramesPerAgreement(inProgressFrames, blockAckAgreementHandler); + for (auto outstandingFrames : outstandingFramesPerAgreement) { if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) return true; } @@ -75,28 +84,26 @@ bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFra } // FIXME -std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const +std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) { - auto largestOutstandingFrames = outstandingFramesPerReceiver.begin(); - for (auto it = outstandingFramesPerReceiver.begin(); it != outstandingFramesPerReceiver.end(); it++) { - if (it->second.size() > largestOutstandingFrames->second.size()) - largestOutstandingFrames = it; - } - MacAddress receiverAddress = largestOutstandingFrames->first; - SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); - Tid tid = largestOutstandingFrames->second.at(0)->peekAtFront()->getTid(); - return std::make_tuple(receiverAddress, startingSequenceNumber, tid); - } + auto outstandingFramesPerAgreement = getOutstandingFramesPerAgreement(inProgressFrames, blockAckAgreementHandler); + auto largestOutstandingFrames = outstandingFramesPerAgreement.end(); + for (auto it = outstandingFramesPerAgreement.begin(); it != outstandingFramesPerAgreement.end(); it++) { + if ((int)it->second.size() >= blockAckReqThreshold && (largestOutstandingFrames == outstandingFramesPerAgreement.end() || it->second.size() > largestOutstandingFrames->second.size())) + largestOutstandingFrames = it; + } + if (largestOutstandingFrames != outstandingFramesPerAgreement.end()) { + MacAddress receiverAddress = largestOutstandingFrames->first.first; + Tid tid = largestOutstandingFrames->first.second; + SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); + return std::make_tuple(receiverAddress, startingSequenceNumber, tid); } return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(), -1); } AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const { - if (agreement == nullptr) + if (agreement == nullptr || agreement->isInactivityExpired()) return AckPolicy::NORMAL_ACK; if (agreement->getIsAddbaResponseReceived() && isBlockAckPolicyEligibleFrame(packet, header)) { if (checkAgreementPolicy(header, agreement)) diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index 5e1b33914ab..94f48ee7979 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -31,7 +31,7 @@ class INET_API OriginatorQosAckPolicy : public ModeSetModuleBase, public IOrigin virtual void initialize(int stage) override; virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const; - virtual std::map> getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const; + virtual std::map, std::vector> getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const; virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const; @@ -39,8 +39,8 @@ class INET_API OriginatorQosAckPolicy : public ModeSetModuleBase, public IOrigin virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const override; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 59bdd8b4372..d764a2c2433 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -178,6 +178,21 @@ void QosAckHandler::processFailedBlockAckReq(const Ptr& header) { if (header->getType() == ST_DATA_WITH_QOS) { @@ -276,6 +291,7 @@ std::string QosAckHandler::getStatusString(Status status) case Status::BLOCK_ACK_ARRIVED_ACKED: return "BLOCK_ACK_ARRIVED_ACKED"; case Status::WAITING_FOR_BLOCK_ACK: return "WAITING_FOR_BLOCK_ACK"; case Status::NORMAL_ACK_ARRIVED: return "NORMAL_ACK_ARRIVED"; + case Status::BLOCK_ACK_NOT_ARRIVED: return "BLOCK_ACK_NOT_ARRIVED"; default: throw cRuntimeError("Unknown status"); } } diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 7c1e0bce61c..c93ea72ac7d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -57,6 +57,10 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void processReceivedAck(const Ptr& ack, const Ptr& ackedHeader); virtual std::set>> processReceivedBlockAck(const Ptr& blockAck); virtual void processFailedBlockAckReq(const Ptr& blockAckReq); + // Makes all outstanding BA-policy frames for this peer/TID retryable + // through the normal acknowledgment path when the agreement is no longer + // available to the data plane. + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid); virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; virtual void processTransmittedDataOrMgmtFrame(const Ptr& header); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 4a42f5f2836..09e1d5ea710 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" #include "inet/common/ModuleAccess.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -62,7 +63,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { if (dynamicPtrCast(blockAckReq)) { - return agreement != nullptr; + return agreement != nullptr && !agreement->isInactivityExpired(); // TODO The Basic BlockAckReq frame shall be discarded if all MSDUs referenced by this // frame have been discarded from the transmit buffer due to expiry of their lifetime limit. } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index f378e533d63..561e19fec5b 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -128,6 +128,20 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data take(dataPacket); expireReceiveLifetime(); // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption + RecipientBlockAckAgreement *agreement = nullptr; + if (dataHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { + if (blockAckAgreementHandler != nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); + if (agreement == nullptr) { + EV_INFO << "Dropping Block Ack policy data without an active Block Ack agreement.\n"; + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, dataPacket, &details); + delete dataPacket; + scheduleReceiveLifetimeTimer(); + return std::vector(); + } + } if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; PacketDropDetails details; @@ -142,7 +156,8 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data if (blockAckReordering && blockAckAgreementHandler) { Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); + if (agreement == nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) { auto processingResult = blockAckReordering->processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); frames = processingResult.frames; @@ -239,7 +254,7 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c if (blockAckReordering) { Tid tid = blockAckReq->getTidInfo(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); + RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); else { diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 296c7cd8510..f7d29ca44a0 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -20,6 +20,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" @@ -33,12 +34,14 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" +#include "inet/linklayer/ieee80211/mac/framesequence/TxOpFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -95,8 +98,10 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosAckPolicy { public: + bool blockAckNeeded = false; + virtual bool isAckNeeded(const Ptr&) const override { return true; } - virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *) const override { return false; } + virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *agreement) const override { return blockAckNeeded && agreement != nullptr; } virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } virtual simtime_t computeBasicBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } }; @@ -133,6 +138,7 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler recipientInactivityDeadline = deadline; } virtual void scheduleAddbaResponseTimer(simtime_t deadline) override { addbaDeadline = deadline; } + virtual bool releaseBlockAckAgreementFrames(MacAddress, Tid) override { return false; } virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { cancelledTransactionIds.push_back(transactionId); excludedPackets.push_back(excludedPacket); @@ -144,6 +150,17 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler } }; +class TestControlResponseCallback : public TestCallback +{ + public: + int numControlResponses = 0; + + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numControlResponses++; + delete responsePacket; + } +}; + class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler { public: @@ -163,6 +180,15 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void publishInactivityDeadline(IBlockAckAgreementHandlerCallback *callback) { scheduleInactivityTimer(callback); } }; +class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy +{ + public: + void configureBlockAckSelection(int maxFrameLength, int threshold) { + maxBlockAckPolicyFrameLength = maxFrameLength; + blockAckReqThreshold = threshold; + } +}; + class TestTimeoutCancellationCallback : public TestCallback { public: @@ -183,6 +209,13 @@ class TestRecipientHandler : public RecipientBlockAckAgreementHandler int getNumCachedAddbaResponses() const { return lastAddbaResponses.size(); } }; +class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure +{ + public: + int getNumReceivedBlockAckReq() const { return numReceivedBlockAckReq; } + int getNumSentBlockAck() const { return numSentBlockAck; } +}; + class TestBlockAckReordering : public BlockAckReordering { public: @@ -633,6 +666,7 @@ class TestHcf : public Hcf std::vector cancelledBlockAckTeardownGenerationIds; int numRebuildEligibilityCalls = 0; int numTransmittedControlResponses = 0; + bool managementProcessedDuringBlockAckExpiry = false; bool droppedSetupCancelled = false; bool delegateDroppedSetupHandling = false; TestCallback *managementCallback = nullptr; @@ -696,6 +730,11 @@ class TestHcf : public Hcf auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); originatorProcessReceivedControlFrame(packet, header, lastTransmittedPacket, lastTransmittedHeader, accessCategory); } + void processInactivityTimeout() { + if (inactivityTimer == nullptr) + inactivityTimer = new cMessage("BlockAckInactivityTimer"); + handleMessage(inactivityTimer); + } protected: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { @@ -703,6 +742,7 @@ class TestHcf : public Hcf delete responsePacket; } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementProcessedDuringBlockAckExpiry |= blockAckInactivityExpiryInProgress; if (managementCallback != nullptr) managementCallback->processMgmtFrame(packet, header); else @@ -2628,28 +2668,28 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); ASSERT(agreement != nullptr); - auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte, AckPolicy ackPolicy) { auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); dataHeader->setTransmitterAddress(transmitterAddress); dataHeader->setFragmentNumber(fragmentNumber); dataHeader->setMoreFragments(moreFragments); - dataHeader->setAckPolicy(BLOCK_ACK); + dataHeader->setAckPolicy(ackPolicy); auto packet = new Packet(name, dataHeader); packet->insertAtBack(makeShared(std::vector({ payloadByte }))); packet->insertAtBack(makeShared()); return packet; }; - auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71); + auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71, BLOCK_ACK); ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); - auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72); + auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72, BLOCK_ACK); ASSERT(service->dataFrameReceived(reorderFinalFragment, reorderFinalFragment->peekAtFront(), &agreementHandler).empty()); ASSERT(dropListener.numSignals == 0); wait(0.5e-6); // No agreement for peer3: this fragment is retained by BasicReassembly, // with a later deadline than the already retained reorder fragments. - auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73); + auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73, NORMAL_ACK); ASSERT(service->dataFrameReceived(basicFragment, basicFragment->peekAtFront(), &agreementHandler).empty()); wait(1.6e-6); ASSERT(dropListener.numSignals == 2); @@ -2658,7 +2698,7 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; // The expired sequence remains tombstoned until the reorder window moves // past it. A late fragment is rejected and reported once by the service, // while no hybrid or partial frame is delivered. - auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74); + auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74, BLOCK_ACK); ASSERT(service->dataFrameReceived(lateReorderFragment, lateReorderFragment->peekAtFront(), &agreementHandler).empty()); ASSERT(dropListener.numSignals == 3); ASSERT(dropListener.numOtherPacketDrops == 3); @@ -2692,7 +2732,7 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; dataHeader->setTransmitterAddress(transmitterAddress); dataHeader->setFragmentNumber(fragmentNumber); dataHeader->setMoreFragments(moreFragments); - dataHeader->setAckPolicy(BLOCK_ACK); + dataHeader->setAckPolicy(NORMAL_ACK); auto packet = new Packet(name, dataHeader); packet->insertAtBack(makeShared(std::vector({ payloadByte }))); packet->insertAtBack(makeShared()); @@ -5615,6 +5655,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) request->setBlockAckTimeoutValue(3); auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); ASSERT(agreement != nullptr); + ASSERT(recipientHandler.getActiveAgreement(12, peer2) == agreement); auto creationTime = simTime(); ASSERT(creationTime > 0); ASSERT(agreement->getExpirationTime() == creationTime + 3); @@ -5634,6 +5675,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) TestCallback expiryCallback; recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); ASSERT(recipientHandler.getAgreement(12, peer2) == agreement); + ASSERT(recipientHandler.getActiveAgreement(12, peer2) == nullptr); ASSERT(agreement->isInactivityExpired()); ASSERT(expiryCallback.managementPackets.size() == 1); ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); @@ -5655,6 +5697,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) TestOriginatorHandler originatorHandler; originatorHandler.addEstablishedAgreement(peer1, 13); auto originatorAgreement = originatorHandler.getAgreement(peer1, 13); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == originatorAgreement); originatorAgreement->setBlockAckTimeoutValue(2); originatorAgreement->calculateExpirationTime(); auto originatorCreationTime = simTime(); @@ -5664,6 +5707,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(3); originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); ASSERT(originatorHandler.getAgreement(peer1, 13) == originatorAgreement); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == nullptr); ASSERT(originatorAgreement->isInactivityExpired()); ASSERT(originatorCallback.managementPackets.size() == 1); ASSERT(originatorCallback.originatorInactivityDeadline == SIMTIME_MAX); @@ -5684,6 +5728,255 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) } +// Expiry quarantines the retained originator agreement from ACK/BAR selection. +// HCF also releases both kinds of outstanding Block Ack state before resuming +// channel access, so those frames can be retried with Normal Ack. +{ + auto originatorHandler = new TestOriginatorHandler(); + originatorHandler->addEstablishedAgreement(peer1, 4); + auto agreement = originatorHandler->getAgreement(peer1, 4); + agreement->setBlockAckTimeoutValue(1); + agreement->calculateExpirationTime(); + + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.configureBlockAckSelection(1000, 1); + auto makeDataPacket = [&](const char *name, int sequenceNumber, AckPolicy ackPolicyValue) { + auto header = makeQosHeader(peer1, 4, SequenceNumberCyclic(sequenceNumber)); + header->setAckPolicy(ackPolicyValue); + return new Packet(name, header); + }; + + auto waitingFrame = makeDataPacket("expiryWaitingForBlockAck", 500, BLOCK_ACK); + auto waitingHeader = waitingFrame->peekAtFront(); + auto notRequestedFrame = makeDataPacket("expiryBlockAckNotRequested", 501, BLOCK_ACK); + auto notRequestedHeader = notRequestedFrame->peekAtFront(); + auto eligibleFrame = makeDataPacket("expiryEligibleFrame", 502, NORMAL_ACK); + auto eligibleHeader = eligibleFrame->peekAtFront(); + auto completingFrame = makeDataPacket("expiryDuringTransmission", 503, BLOCK_ACK); + auto completingHeader = completingFrame->peekAtFront(); + ASSERT(ackPolicy.computeAckPolicy(eligibleFrame, eligibleHeader, agreement) == BLOCK_ACK); + + TestOriginatorQosMacDataService dataService; + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + inProgressFrames.addFrame(waitingFrame); + inProgressFrames.addFrame(notRequestedFrame); + inProgressFrames.addFrame(eligibleFrame); + inProgressFrames.addFrame(completingFrame); + ackHandler.frameGotInProgress(waitingHeader); + ackHandler.processTransmittedDataOrMgmtFrame(waitingHeader); + auto blockAckReq = makeShared(); + blockAckReq->setReceiverAddress(peer1); + blockAckReq->setTidInfo(4); + blockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(500)); + ackHandler.processTransmittedBlockAckReq(blockAckReq); + ackHandler.frameGotInProgress(notRequestedHeader); + ackHandler.processTransmittedDataOrMgmtFrame(notRequestedHeader); + ackHandler.frameGotInProgress(completingHeader); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + + TestTxopProcedure txopProcedure; + FrameSequenceContext context(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, nullptr, + new QoSContext(&ackPolicy, nullptr, originatorHandler, &txopProcedure)); + TxOpFs txOpFs; + ASSERT(txOpFs.selectTxOpSequence(&txOpFs, &context) == 2); + + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback managementCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureManagementCallback(&managementCallback); + + wait(1); + hcf.processInactivityTimeout(); + ASSERT(originatorHandler->getAgreement(peer1, 4) == agreement); + ASSERT(originatorHandler->getActiveAgreement(peer1, 4) == nullptr); + ASSERT(agreement->isInactivityExpired()); + ASSERT(managementCallback.managementPackets.size() == 1); + ASSERT(hcf.managementProcessedDuringBlockAckExpiry); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + hcf.processTransmittedData(completingFrame, completingHeader, AC_BE); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(waitingHeader)); + ASSERT(ackHandler.isEligibleToTransmit(notRequestedHeader)); + ASSERT(ackHandler.isEligibleToTransmit(completingHeader)); + ASSERT(hcf.numRebuildEligibilityCalls == 1); + ASSERT(hcf.numResumedEligibleChannelAccess == 1); + ASSERT(ackPolicy.computeAckPolicy(eligibleFrame, eligibleHeader, agreement) == NORMAL_ACK); + ASSERT(txOpFs.selectTxOpSequence(&txOpFs, &context) == 1); + + auto lateBlockAck = makeShared(); + lateBlockAck->setTransmitterAddress(peer1); + lateBlockAck->setTidInfo(4); + lateBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(500)); + auto lateBlockAckPacket = new Packet("postExpiryBlockAck", lateBlockAck); + auto transmittedBlockAckReqPacket = new Packet("preExpiryBlockAckReq", blockAckReq); + hcf.processReceivedAck(lateBlockAckPacket, transmittedBlockAckReqPacket, AC_BE); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + delete lateBlockAckPacket; + delete transmittedBlockAckReqPacket; + + auto frames = inProgressFrames.releaseFrames(); + for (auto frame : frames) + delete frame; +} + +// Any terminal originator agreement removal releases outstanding BA-policy +// frames as well. Otherwise active-agreement BAR filtering would leave those +// frames permanently ineligible after peer or local DELBA teardown. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.configureBlockAckSelection(1000, 1); + TestOriginatorQosMacDataService dataService; + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + originatorHandler->addEstablishedAgreement(peer1, 6); + auto peerTeardownHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(520)); + peerTeardownHeader->setAckPolicy(BLOCK_ACK); + auto peerTeardownFrame = new Packet("peerDelbaOutstandingData", peerTeardownHeader); + inProgressFrames.addFrame(peerTeardownFrame); + ackHandler.frameGotInProgress(peerTeardownHeader); + ackHandler.processTransmittedDataOrMgmtFrame(peerTeardownHeader); + ASSERT(ackHandler.getQoSDataAckStatus(peerTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + auto peerDelba = makeShared(); + peerDelba->setTransmitterAddress(peer1); + peerDelba->setTid(6); + peerDelba->setInitiator(false); + hcf.processReceivedManagementFrame(peerDelba); + ASSERT(originatorHandler->getAgreement(peer1, 6) == nullptr); + ASSERT(ackHandler.getQoSDataAckStatus(peerTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(peerTeardownHeader)); + + originatorHandler->addEstablishedAgreement(peer1, 7); + auto localTeardownAgreement = originatorHandler->getAgreement(peer1, 7); + auto localTeardownHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(530)); + localTeardownHeader->setAckPolicy(BLOCK_ACK); + auto localTeardownFrame = new Packet("localDelbaOutstandingData", localTeardownHeader); + inProgressFrames.addFrame(localTeardownFrame); + ackHandler.frameGotInProgress(localTeardownHeader); + ackHandler.processTransmittedDataOrMgmtFrame(localTeardownHeader); + ASSERT(ackHandler.getQoSDataAckStatus(localTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + auto localDelbaPacket = makeTaggedDelbaPacket("localTerminalDelba", peer1, 7, SequenceNumberCyclic(531), 0, false, localTeardownAgreement->getTransactionId()); + auto localDelba = localDelbaPacket->peekAtFront(); + hcf.processTransmittedManagement(localDelbaPacket, localDelba, AC_BE); + ASSERT(originatorHandler->getAgreement(peer1, 7) == nullptr); + ASSERT(ackHandler.getQoSDataAckStatus(localTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(localTeardownHeader)); + delete localDelbaPacket; + + auto frames = inProgressFrames.releaseFrames(); + for (auto frame : frames) + delete frame; +} + +// An expired recipient agreement remains available to the DELBA lifecycle, +// but BA-policy data is dropped, BAR cannot release reordering state, and no +// Block Ack response is generated. +{ + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 1; + TestCallback setupCallback; + auto request = makeRequest(peer2, 5, 111, SequenceNumberCyclic(600), 32); + request->setBlockAckTimeoutValue(1); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); + ASSERT(agreement != nullptr); + ASSERT(recipientHandler.getActiveAgreement(5, peer2) == agreement); + + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("expiredAgreementDataService", this); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + TestPacketDropSignalListener dropListener; + service->subscribe(packetDroppedSignal, &dropListener); + + auto bufferedHeader = makeQosHeader(peer1, 5, SequenceNumberCyclic(601)); + bufferedHeader->setTransmitterAddress(peer2); + bufferedHeader->setAckPolicy(BLOCK_ACK); + auto bufferedPacket = new Packet("preExpiryBufferedData", bufferedHeader); + bufferedPacket->insertAtBack(makeShared()); + ASSERT(service->dataFrameReceived(bufferedPacket, bufferedHeader, &recipientHandler).empty()); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(601), 0)); + + auto blockAckReq = makeShared(); + blockAckReq->setTransmitterAddress(peer2); + blockAckReq->setReceiverAddress(peer1); + blockAckReq->setTidInfo(5); + blockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(601)); + auto blockAckReqPacket = new Packet("expiredAgreementBar", blockAckReq); + RecipientQosAckPolicy productionAckPolicy; + ASSERT(productionAckPolicy.isBlockAckNeeded(blockAckReq, agreement)); + TestRecipientAckPolicy ackPolicy; + ackPolicy.blockAckNeeded = true; + TestRecipientBlockAckProcedure blockAckProcedure; + TestControlResponseCallback responseCallback; + blockAckProcedure.processReceivedBlockAckReq(blockAckReqPacket, blockAckReq, &ackPolicy, &recipientHandler, &responseCallback); + ASSERT(responseCallback.numControlResponses == 1); + + wait(1); + TestCallback expiryCallback; + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(recipientHandler.getAgreement(5, peer2) == agreement); + ASSERT(recipientHandler.getActiveAgreement(5, peer2) == nullptr); + ASSERT(agreement->isInactivityExpired()); + ASSERT(!productionAckPolicy.isBlockAckNeeded(blockAckReq, agreement)); + + auto expiredHeader = makeQosHeader(peer1, 5, SequenceNumberCyclic(602)); + expiredHeader->setTransmitterAddress(peer2); + expiredHeader->setAckPolicy(BLOCK_ACK); + auto expiredPacket = new Packet("postExpiryBlockAckData", expiredHeader); + expiredPacket->insertAtBack(makeShared()); + ASSERT(service->dataFrameReceived(expiredPacket, expiredHeader, &recipientHandler).empty()); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + ASSERT(!agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(602), 0)); + + ASSERT(service->controlFrameReceived(blockAckReqPacket, blockAckReq, &recipientHandler).empty()); + ASSERT(dropListener.numSignals == 1); + blockAckProcedure.processReceivedBlockAckReq(blockAckReqPacket, blockAckReq, &ackPolicy, &recipientHandler, &responseCallback); + ASSERT(responseCallback.numControlResponses == 1); + + service->resetBlockAckReordering(5, peer2); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); + service->unsubscribe(packetDroppedSignal, &dropListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); + delete blockAckReqPacket; +} + // A timeout DELBA can be aborted before its final fragment is transmitted. // The matching generation is retired locally, and the returned ownership lets // HCF release role-specific resources without rearming a timed-out agreement. diff --git a/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test b/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test index 741d6cc8975..1e1d93fc19c 100644 --- a/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test +++ b/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test @@ -40,6 +40,11 @@ class ExpiryCallback : public IProcedureCallback, public IBlockAckAgreementHandl virtual void scheduleAddbaResponseTimer(simtime_t) override {} virtual void cancelAddbaTransaction(uint64_t, Packet *) override {} virtual void cancelBlockAckTeardown(bool, MacAddress, Tid, uint64_t, Packet *) override {} + virtual bool releaseBlockAckAgreementFrames(MacAddress address, Tid tid) override { + if (action == 3 && tid == 0) + handler->install(address, tid, 100, SimTime(10, SIMTIME_MS)); + return false; + } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { auto delba = dynamicPtrCast(header); ASSERT(delba != nullptr); @@ -62,7 +67,7 @@ class ExpiryCallback : public IProcedureCallback, public IBlockAckAgreementHandl }; %activity: -for (int action = 0; action < 3; action++) { +for (int action = 0; action < 4; action++) { ExpiryHandler handler; ExpiryCallback callback; callback.handler = &handler; @@ -74,18 +79,16 @@ for (int action = 0; action < 3; action++) { handler.install(callback.peer, 2, 3, SimTime(20, SIMTIME_MS)); handler.install(callback.peer, 3, 4, 0); wait(SimTime(2, SIMTIME_MS)); - handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(handler.blockAckAgreementExpired(&callback, &callback)); ASSERT(callback.sent == (action == 0 ? 2 : 1)); ASSERT(handler.contains(callback.peer, 0) == (action >= 2)); ASSERT(handler.contains(callback.peer, 1) == (action == 2)); ASSERT(handler.contains(callback.peer, 2)); ASSERT(handler.contains(callback.peer, 3)); ASSERT(callback.deadline == start + SimTime(action >= 2 ? 12 : 20, SIMTIME_MS)); - handler.blockAckAgreementExpired(&callback, &callback); - ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); for (int tid = 0; tid < 4; tid++) handler.erase(callback.peer, tid); - handler.blockAckAgreementExpired(&callback, &callback); - ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); ASSERT(callback.deadline == SIMTIME_MAX); } diff --git a/tests/unit/Ieee80211RecipientExpiryReentrant_1.test b/tests/unit/Ieee80211RecipientExpiryReentrant_1.test index e9c00024597..516864c26f7 100644 --- a/tests/unit/Ieee80211RecipientExpiryReentrant_1.test +++ b/tests/unit/Ieee80211RecipientExpiryReentrant_1.test @@ -38,6 +38,11 @@ class ExpiryCallback : public IProcedureCallback, public IBlockAckAgreementHandl virtual void scheduleAddbaResponseTimer(simtime_t) override {} virtual void cancelAddbaTransaction(uint64_t, Packet *) override {} virtual void cancelBlockAckTeardown(bool, MacAddress, Tid, uint64_t, Packet *) override {} + virtual bool releaseBlockAckAgreementFrames(MacAddress address, Tid tid) override { + if (action == 3 && tid == 0) + handler->install(address, tid, 100, SimTime(10, SIMTIME_MS)); + return false; + } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { auto delba = dynamicPtrCast(header); ASSERT(delba != nullptr); @@ -72,18 +77,16 @@ for (int action = 0; action < 3; action++) { handler.install(callback.peer, 2, 3, SimTime(20, SIMTIME_MS)); handler.install(callback.peer, 3, 4, 0); wait(SimTime(2, SIMTIME_MS)); - handler.blockAckAgreementExpired(&callback, &callback); + ASSERT(handler.blockAckAgreementExpired(&callback, &callback)); ASSERT(callback.sent == (action == 0 ? 2 : 1)); ASSERT(handler.contains(callback.peer, 0) == (action >= 2)); ASSERT(handler.contains(callback.peer, 1) == (action == 2)); ASSERT(handler.contains(callback.peer, 2)); ASSERT(handler.contains(callback.peer, 3)); ASSERT(callback.deadline == start + SimTime(action >= 2 ? 12 : 20, SIMTIME_MS)); - handler.blockAckAgreementExpired(&callback, &callback); - ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); for (int tid = 0; tid < 4; tid++) handler.erase(callback.peer, tid); - handler.blockAckAgreementExpired(&callback, &callback); - ASSERT(callback.sent == (action == 0 ? 2 : 1)); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); ASSERT(callback.deadline == SIMTIME_MAX); } From c410c0e89521cfcab1b4986b5208400bf14bc040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:49:20 +0200 Subject: [PATCH 38/91] ieee80211: fix: skip incomplete data released by a BAR When a BAR releases data fragments that do not reassemble into a packet, recipient processing skips data dispatch instead of dereferencing a null result. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac.RecipientQosMacDataService | behavior.change.fix | test whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 5 + .../recipient/RecipientQosMacDataService.cc | 4 +- tests/unit/Ieee80211AddbaTransaction_1.test | 106 ++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/WHATSNEW b/WHATSNEW index c49dafcff89..02af53ecdcb 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -422,6 +422,11 @@ Notable backward incompatible changes are the following: acknowledgment/recovery paths before teardown proceeds, including when callbacks replace agreement state. +12. Incomplete Data Released by a Block Ack Request + + When a BAR releases data fragments that do not reassemble into a packet, recipient + processing skips data dispatch instead of dereferencing a null result. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 561e19fec5b..bb69050ed7e 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -266,7 +266,9 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c if (basicReassembly) { // FIXME defragmentation for (auto it : frames) { auto fragments = it.second; - defragmentedFrames.push_back(defragment(fragments)); + auto frame = defragment(fragments); + if (frame != nullptr) + defragmentedFrames.push_back(frame); } } else { diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index f7d29ca44a0..6f5b2a56149 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -2545,6 +2545,112 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } +// A complete fragmented MPDU released by a BAR at the exact sequence-space +// half boundary must be consumed safely when BasicReassembly rejects its +// ambiguous generation. The production QoS data service must not expose the +// resulting null packet to deaggregation or retain the released fragments. +{ + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("halfSpaceBarDataService", this); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + RecipientBlockAckAgreementHandler agreementHandler; + TestRecipientPolicy policy; + TestCallback callback; + + // Establish the BasicReassembly sequence-space high watermark at zero + // before the Block Ack receive buffer is created. + auto seedHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(0)); + seedHeader->setTransmitterAddress(peer1); + seedHeader->setAckPolicy(NORMAL_ACK); + seedHeader->setAMsduPresent(false); + auto seedPacket = new Packet("halfSpaceReassemblySeed", seedHeader); + auto seedFrames = service->dataFrameReceived(seedPacket, seedHeader, &agreementHandler); + ASSERT(seedFrames.size() == 1); + ASSERT(seedFrames.front() == seedPacket); + for (auto frame : seedFrames) { + take(frame); + drop(frame); + delete frame; + } + + auto request = makeRequest(peer1, 6, 94, SequenceNumberCyclic(2047), 64); + request->setReceiverAddress(peer2); + auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); + ASSERT(agreement != nullptr); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(2047)); + + auto makeFragment = [&](const char *name, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto header = makeQosHeader(peer2, 6, SequenceNumberCyclic(2048)); + header->setTransmitterAddress(peer1); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + header->setAckPolicy(BLOCK_ACK); + header->setAMsduPresent(false); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto firstFragment = makeFragment("halfSpaceBarFragment0", 0, true, 0x81); + auto secondFragment = makeFragment("halfSpaceBarFragment1", 1, false, 0x82); + ASSERT(service->dataFrameReceived(firstFragment, firstFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(service->dataFrameReceived(secondFragment, secondFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2048), 0)); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2048), 1)); + + TestSignalListener defragmentedListener; + TestSignalListener deaggregatedListener; + TestPacketDropSignalListener resetDropListener; + service->subscribe(IRecipientQosMacDataService::packetDefragmentedSignal, &defragmentedListener); + service->subscribe(IRecipientQosMacDataService::packetDeaggregatedSignal, &deaggregatedListener); + service->subscribe(packetDroppedSignal, &resetDropListener); + + auto barHeader = makeShared(); + barHeader->setTransmitterAddress(peer1); + barHeader->setReceiverAddress(peer2); + barHeader->setTidInfo(6); + barHeader->setStartingSequenceNumber(SequenceNumberCyclic(2048)); + auto barPacket = new Packet("halfSpaceBar", barHeader); + auto releasedFrames = service->controlFrameReceived(barPacket, barHeader, &agreementHandler); + ASSERT(releasedFrames.empty()); + ASSERT(defragmentedListener.numSignals == 0); + ASSERT(deaggregatedListener.numSignals == 0); + delete barPacket; + + // getAckState() intentionally reports older or absent sequence controls + // as acknowledged, so it cannot expose removal of sequence 2048. A + // complete next sequence released immediately proves that the BAR did + // advance the reorder window and the new Block Ack state is recorded. + auto nextHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(2049)); + nextHeader->setTransmitterAddress(peer1); + nextHeader->setAckPolicy(BLOCK_ACK); + nextHeader->setAMsduPresent(false); + auto nextPacket = new Packet("halfSpaceBarFollowingFrame", nextHeader); + auto nextFrames = service->dataFrameReceived(nextPacket, nextHeader, &agreementHandler); + ASSERT(nextFrames.size() == 1); + ASSERT(nextFrames.front() == nextPacket); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2049), 0)); + ASSERT(defragmentedListener.numSignals == 1); + ASSERT(deaggregatedListener.numSignals == 0); + for (auto frame : nextFrames) { + take(frame); + drop(frame); + delete frame; + } + + // resetBlockAckReordering is a public owner-side observation: no drops + // prove that the BAR already removed the complete MPDU from the reorder + // buffer before BasicReassembly consumed its rejected fragments. + service->resetBlockAckReordering(6, peer1); + ASSERT(resetDropListener.numSignals == 0); + service->unsubscribe(packetDroppedSignal, &resetDropListener); + service->unsubscribe(IRecipientQosMacDataService::packetDeaggregatedSignal, &deaggregatedListener); + service->unsubscribe(IRecipientQosMacDataService::packetDefragmentedSignal, &defragmentedListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); +} + // Receive-buffer lifetime starts when the first fragment of an incomplete // fragmented MPDU is retained. Later fragments do not refresh that deadline; // complete MPDUs held only for ordering remain available until normal window From fe1ded836557c885fa68c6e105504178f1c478b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:50:34 +0200 Subject: [PATCH 39/91] ieee80211: fix: honor recipient Block Ack timeout policy The timeout-policy ternary selected the wrong branch. A nonzero recipient policy overrides the advisory request timeout; the model's zero-policy convention inherits it. Cover both choices and the resulting inactivity deadlines independently of BAR handling. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac.RecipientBlockAckAgreementHandler | behavior.change.fix | test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 6 ++ doc/src/migration-guide/index.rst | 8 +++ .../RecipientBlockAckAgreementHandler.cc | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 57 +++++++++++++++---- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 02af53ecdcb..2d1e11769f3 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -427,6 +427,12 @@ Notable backward incompatible changes are the following: When a BAR releases data fragments that do not reassemble into a packet, recipient processing skips data dispatch instead of dereferencing a null result. +13. Recipient Block Ack Timeout Policy + + Recipient Block Ack timeout selection now honors a nonzero local policy value; zero + inherits the request's advisory value. This corrects the previously reversed choice + and can change agreement expiry and subsequent traffic. + 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 97892e472d6..ebf92c6b2a5 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -189,6 +189,14 @@ synchronously remove or replace an agreement, so relookup the original generatio generating its DELBA. If an implementation has no outstanding frames, returning false must be an explicit implementation decision. +Recipient Block Ack Timeout Policy +---------------------------------- + +A nonzero recipient ``blockAckTimeoutValue`` policy overrides the advisory timeout in +the ADDBA request. The model's zero-policy convention inherits the request timeout. +Check configurations that previously depended on the reversed selection; the negotiated +inactivity deadline may change. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 35c96fbfac3..81d06d247f7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -130,7 +130,7 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR addbaResponse->setTid(tid); addbaResponse->setBlockAckPolicy(!addbaRequest->getBlockAckPolicy() && blockAckAgreementPolicy->delayedBlockAckPolicySupported() ? false : true); addbaResponse->setBufferSize(addbaRequest->getBufferSize() <= blockAckAgreementPolicy->getMaximumAllowedBufferSize() ? addbaRequest->getBufferSize() : blockAckAgreementPolicy->getMaximumAllowedBufferSize()); - addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? blockAckAgreementPolicy->getBlockAckTimeoutValue() : addbaRequest->getBlockAckTimeoutValue()); + addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? addbaRequest->getBlockAckTimeoutValue() : blockAckAgreementPolicy->getBlockAckTimeoutValue()); addbaResponse->setAMsduSupported(blockAckAgreementPolicy->aMsduSupported()); return addbaResponse; } diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 6f5b2a56149..3ad7ac8a115 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -1881,6 +1881,27 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(threw); } +// IEEE Std 802.11-2024, 10.25.2: the recipient policy may inherit the +// originator's advisory timeout when it does not configure an override. The +// response, installed agreement, and inactivity deadline must use that one +// negotiated value. +{ + RecipientBlockAckAgreementHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + auto request = makeRequest(peer1, 8, 93, SequenceNumberCyclic(200), 32); + request->setBlockAckTimeoutValue(3); + auto agreementCreationTime = simTime(); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &responseCallback, &responseCallback); + ASSERT(agreement != nullptr); + auto response = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(response != nullptr); + ASSERT(response->getBlockAckTimeoutValue() == 3); + ASSERT(agreement->getBlockAckTimeoutValue() == 3); + ASSERT(agreement->getExpirationTime() == agreementCreationTime + 3); + ASSERT(responseCallback.recipientInactivityDeadline == agreementCreationTime + 3); +} + // An initially rejected request has no agreement, so its response is never // replayable and must not consume replay-cache state. { @@ -1950,6 +1971,7 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto firstRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); firstRequest->setSequenceNumber(SequenceNumberCyclic(50)); firstRequest->setBlockAckTimeoutValue(3); + auto firstAgreementCreationTime = simTime(); auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); ASSERT(!firstReception.duplicate); ASSERT(firstReception.completeFrames.empty()); @@ -1970,8 +1992,10 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(firstResponse->getTid() == 6); ASSERT(firstResponse->getBlockAckPolicy()); ASSERT(firstResponse->getBufferSize() == 32); - ASSERT(firstResponse->getBlockAckTimeoutValue() == 3); + ASSERT(firstResponse->getBlockAckTimeoutValue() == 1); ASSERT(firstResponse->getAMsduSupported()); + ASSERT(firstAgreement->getBlockAckTimeoutValue() == 1); + ASSERT(firstAgreement->getExpirationTime() == firstAgreementCreationTime + 1); auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(101)); bufferedHeader->setTransmitterAddress(peer2); @@ -2029,7 +2053,7 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(rejectedResponse->getStatusCode() != 0); ASSERT(rejectedResponse->getTid() == 6); ASSERT(rejectedResponse->getBufferSize() == 1); - ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 4); + ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 1); ASSERT(!rejectedResponse->getAMsduSupported()); auto duplicateRejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); @@ -2424,8 +2448,13 @@ ASSERT(firstImmediateAgreement != nullptr); ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == firstImmediateAgreement); ASSERT(firstImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(0)); auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); +ASSERT(acceptedResponse != nullptr); ASSERT(acceptedResponse->getDialogToken() == 77); ASSERT(acceptedResponse->getStatusCode() == 0); +ASSERT(acceptedResponse->getBlockAckTimeoutValue() == 0); +ASSERT(firstImmediateAgreement->getBlockAckTimeoutValue() == 0); +ASSERT(firstImmediateAgreement->getExpirationTime() == SIMTIME_MAX); +ASSERT(recipientCallback.recipientInactivityDeadline == SIMTIME_MAX); // Renegotiation replaces the recipient agreement and the HCF reset boundary // must discard the old reorder window before frames use the new SSN. @@ -5699,14 +5728,14 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) { TestRecipientHandler expiryHandler; TestRecipientPolicy expiryPolicy; - expiryPolicy.blockAckTimeoutValue = 1; + expiryPolicy.blockAckTimeoutValue = 2; TestCallback expiryCallback; auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); - request->setBlockAckTimeoutValue(1); + request->setBlockAckTimeoutValue(3); auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); ASSERT(firstAgreement != nullptr); auto firstGenerationId = firstAgreement->getGenerationId(); - wait(1); + wait(2); expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); auto stalePacket = expiryCallback.managementPacket; auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); @@ -5755,17 +5784,23 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(1); TestRecipientHandler recipientHandler; TestRecipientPolicy recipientPolicy; - recipientPolicy.blockAckTimeoutValue = 3; + recipientPolicy.blockAckTimeoutValue = 2; TestCallback setupCallback; auto request = makeRequest(peer2, 12, 109, SequenceNumberCyclic(330), 32); request->setBlockAckTimeoutValue(3); + auto agreementCreationTime = simTime(); auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); ASSERT(agreement != nullptr); ASSERT(recipientHandler.getActiveAgreement(12, peer2) == agreement); auto creationTime = simTime(); ASSERT(creationTime > 0); - ASSERT(agreement->getExpirationTime() == creationTime + 3); - ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 3); + auto response = dynamicPtrCast(setupCallback.managementHeader); + ASSERT(response != nullptr); + ASSERT(response->getBlockAckTimeoutValue() == 2); + ASSERT(agreement->getBlockAckTimeoutValue() == 2); + ASSERT(agreement->getExpirationTime() == agreementCreationTime + 2); + ASSERT(agreement->getExpirationTime() == creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 2); auto basicBlockAckReq = makeShared(); basicBlockAckReq->setTransmitterAddress(peer2); @@ -5773,9 +5808,9 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(1); auto barReceptionTime = simTime(); recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); - ASSERT(agreement->getExpirationTime() == barReceptionTime + 3); - ASSERT(agreement->getExpirationTime() > creationTime + 3); - ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 3); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 2); + ASSERT(agreement->getExpirationTime() > creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 2); wait(4); TestCallback expiryCallback; From 4e448ecf3a4ff9e9abb0131fc44c0478a621d86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:51:40 +0200 Subject: [PATCH 40/91] ieee80211: fix: enforce valid A-MSDU fragmentation and sizing Basic A-MSDUs must fit the configured frame limit with subframe headers and padding, and cannot use this policy's ordinary fragmentation path. Treat -1 as an unlimited aggregation size while enforcing finite limits on the serialized body. Correct serialized padding changes aggregate selection; ordinary A-MSDU fragmentation is suppressed. Retain the accepted eight-row update and refresh MacQosWithBlockAck on rebased master 7287f347aa, which includes the corrected BAR timing. Among the seven selected Block Ack cases, only this row changes at this boundary. Its preceding intermediate value belongs to the originator ADDBA commit; this commit owns the final value. Tests cover exact padding bounds, unlimited size and A-MSDU versus ordinary-data fragmentation. The user approved these exact refreshed expectations after the 2026-09-15 attribution report. Scoped debug verification uses run 0 and all declared ingredients. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac | behavior.change.fix | fingerprint test migration whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 7 ++ doc/src/migration-guide/index.rst | 8 ++ .../aggregation/BasicMsduAggregationPolicy.cc | 22 +++- .../fragmentation/BasicFragmentationPolicy.cc | 8 +- tests/fingerprint/examples.csv | 10 +- tests/fingerprint/showcases.csv | 6 +- tests/unit/Ieee80211AddbaTransaction_1.test | 107 ++++++++++++++++++ 7 files changed, 154 insertions(+), 14 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 2d1e11769f3..9a003eb6082 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -433,6 +433,13 @@ Notable backward incompatible changes are the following: inherits the request's advisory value. This corrects the previously reversed choice and can change agreement expiry and subsequent traffic. +14. A-MSDU Size and Fragmentation Limits + + Basic A-MSDUs now include subframe headers and padding in size checks and avoid the + ordinary fragmentation path. Eligible members retain flow order. Aggregation, + transmissions and delivered traffic may change in configurations that previously + built oversized or fragmentable A-MSDUs. + 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 ebf92c6b2a5..aa079db7c13 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -197,6 +197,14 @@ the ADDBA request. The model's zero-policy convention inherits the request timeo Check configurations that previously depended on the reversed selection; the negotiated inactivity deadline may change. +A-MSDU Size and Fragmentation Limits +------------------------------------ + +Basic A-MSDU selection must fit the configured limit including subframe headers and +padding. It must not rely on ordinary MPDU fragmentation to split an oversized A-MSDU. +Check aggregation and fragmentation thresholds together when reproducing a study that +previously selected such aggregates. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index e71051047bb..c37493fb4c2 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc @@ -10,6 +10,16 @@ namespace inet { namespace ieee80211 { +namespace { + +b computeSerializedAmsduLength(b aMsduLength, const Packet *packet, const Ptr& header, const Ptr& trailer) +{ + int paddingLength = (4 - aMsduLength.get() % 4) % 4; + return aMsduLength + B(paddingLength) + packet->getDataLength() - header->getChunkLength() - trailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); +} + +} + Define_Module(BasicMsduAggregationPolicy); void BasicMsduAggregationPolicy::initialize() @@ -32,10 +42,12 @@ bool BasicMsduAggregationPolicy::isEligible(Packet *packet, const PtrgetType() != ST_DATA_WITH_QOS) return false; - // The maximum MPDU length that can be transported using A-MPDU aggregation is 4095 octets. An - // A-MSDU cannot be fragmented. Therefore, an A-MSDU of a length that exceeds 4065 octets ( - // 4095 minus the QoS data MPDU overhead) cannot be transported in an A-MPDU. - if (aMsduLength + packet->getDataLength() - header->getChunkLength() - trailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER) > maxAMsduSize) // default value of maxAMsduSize is 4065 + // IEEE Std 802.11-2024, 9.3.2.2.1-9.3.2.2.2 and Figure 9-123: every + // non-final Basic A-MSDU subframe is padded to a 4-octet boundary. The + // final subframe is not padded. An A-MSDU cannot be fragmented by this + // policy, so its complete serialized body must fit the configured limit. + auto serializedAmsduLength = computeSerializedAmsduLength(aMsduLength, packet, header, trailer); + if (maxAMsduSize >= b(0) && serializedAmsduLength > maxAMsduSize) // -1 means infinity return false; // The value of TID present in the QoS Control field of the MPDU carrying the A-MSDU indicates the TID for @@ -91,7 +103,7 @@ std::vector *BasicMsduAggregationPolicy::computeAggregateFrames(queuei if (!isEligible(dataPacket, dataHeader, dataTrailer, firstHeader, aMsduLength)) return false; frames->push_back(dataPacket); - aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); + aMsduLength = computeSerializedAmsduLength(aMsduLength, dataPacket, dataHeader, dataTrailer); return true; }; if (!appendIfEligible(candidate)) { diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index aa741a61a3f..9521f5f9c5f 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -36,7 +36,13 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) return {}; const auto& trailer = frame->peekAtBack(B(4)); int trailerLength = trailer->getChunkLength().get(); - if (dynamicPtrCast(header)) { + if (const auto& dataHeader = dynamicPtrCast(header)) { + // IEEE Std 802.11-2024, 10.2.7 Note 2, 10.4, and 10.11: A-MSDUs are + // fragmented only through the capability-gated HE dynamic + // fragmentation procedure (26.3.2), which this basic policy does + // not implement. + if (dataHeader->getAMsduPresent()) + return {}; headerLength = header->getChunkLength().get(); payloadLength = frame->getByteLength() - headerLength - trailerLength; } diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index afa5323feb4..04a18785762 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -6,8 +6,8 @@ /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping1 -r 0, 1000s, 8668-d4e7/tplx;e2b1-205b/~tNl;4083-075c/~tND;e1f1-f93c/tyf, PASS, wireless adhoc Ipv4 # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, ca86-69c6/tplx;521b-977a/~tNl;f7ee-50fd/~tND;5471-336a/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, e8ad-ca0f/tplx;b738-d1b7/~tNl;9161-07f7/~tND;6e70-47f6/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 7852-a806/tplx;6caf-ac5a/~tNl;28e7-2e1e/~tND;ea34-baa1/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, b403-1b77/tplx;5f3c-e4ec/~tNl;95ae-9a68/~tND;0be3-13a5/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 9c66-e6f0/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 85bd-85c6/tplx;22f6-085d/~tNl;e4ea-962c/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -655,10 +655,10 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;7a95-cf35/~tND, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, c3c0-dc93/tplx;5eb9-6cea/~tNl;87a5-11a1/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, c896-d56c/tplx;0427-432c/~tNl;7327-574f/~tND;ae9f-1da4/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 0256-c54d/tplx;28b8-02ea/~tNl;4253-7970/~tND;7971-c79c/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 33e5-c380/tplx;78d2-bb9a/~tNl;1524-d88f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, d637-2202/tplx;a6ab-03ac/~tNl;9f12-2283/~tND;22b8-1e1c/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 5570-9d59/tplx;c521-a55d/~tNl;e5fc-a44c/~tND;e59c-a8ed/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, c7d5-f1c6/tplx;c4c1-d67d/~tNl;84b7-2069/~tND;2dbe-28bf/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, f7c0-98c4/tplx;c808-a11f/~tNl;e5b6-5b54/~tND;53e0-4b99/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, 4221-b2c3/tplx;f9c2-e9ef/~tNl;5ab0-68ce/~tND, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 975eb111f8d..35bbd970345 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -192,8 +192,8 @@ /showcases/visualizer/canvas/transportpathactivity/, -f omnetpp.ini -c Filtering -r 0, 100s, 8bcf-d426/tplx;8f09-7a80/~tNl;92a4-c3ec/~tND;0694-d5be/tyf, PASS, wireless EthernetMac Ipv4 /showcases/wireless/aggregation/, -f omnetpp.ini -c NoAggregation -r 0, 1s, 97f6-a969/tplx;ecf2-6e33/~tNl;5d9e-8fdf/~tND;27f2-4a52/tyf, PASS, wireless Ipv4 -/showcases/wireless/aggregation/, -f omnetpp.ini -c Aggregation -r 0, 1s, 671b-274c/tplx;6d84-9720/~tNl;b29d-4083/~tND;4292-f684/tyf, PASS, wireless Ipv4 -/showcases/wireless/aggregation/, -f omnetpp.ini -c VoicePriorityAggregation -r 0, 1s, 1dbe-6672/tplx;a253-dbe3/~tNl;3bb2-9797/~tND;f028-4b38/tyf, PASS, wireless Ipv4 +/showcases/wireless/aggregation/, -f omnetpp.ini -c Aggregation -r 0, 1s, 58d0-b4a4/tplx;2c02-b385/~tNl;a6cb-d366/~tND;fa56-c9fa/tyf, PASS, wireless Ipv4 +/showcases/wireless/aggregation/, -f omnetpp.ini -c VoicePriorityAggregation -r 0, 1s, 3f9d-465e/tplx;78b5-09d7/~tNl;4cc7-05ed/~tND;d760-98fe/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 @@ -314,7 +314,7 @@ /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/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 26be-ff05/tplx;9c39-00a3/~tNl;03a9-2f02/~tND;5fef-ddb3/tyf, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, faed-a002/tplx;84a6-b893/~tNl;cb18-5b4d/~tND;6132-2b2c/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 665f-be37/tplx;805c-ea54/~tNl;d745-7405/~tND;afaf-c551/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/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 3ad7ac8a115..07eed429a4c 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -2173,6 +2173,44 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); fragmentationPolicyModule->callFinish(); fragmentationPolicyModule->deleteModule(); + // IEEE Std 802.11-2024, 10.2.7 Note 2, 10.4, and 10.11: A-MSDUs are fragmented + // only with capability-gated HE dynamic fragmentation (26.3.2). The + // basic policy does not implement that procedure, so an oversized + // A-MSDU must remain a single MPDU candidate. + auto makeQosDataPacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto firstMsdu = makeQosDataPacket("oversizedAmsduFirst", SequenceNumberCyclic(110), 20); + auto secondMsdu = makeQosDataPacket("oversizedAmsduSecond", SequenceNumberCyclic(111), 20); + auto amsduMembers = new std::vector({ firstMsdu, secondMsdu }); + MsduAggregation msduAggregation; + auto oversizedAmsdu = msduAggregation.aggregateFrames(amsduMembers); + delete amsduMembers; + const auto& oversizedAmsduHeader = oversizedAmsdu->peekAtFront(); + const auto& oversizedAmsduTrailer = oversizedAmsdu->peekAtBack(B(4)); + ASSERT(oversizedAmsduHeader->getAMsduPresent()); + ASSERT((oversizedAmsdu->getDataLength() - oversizedAmsduHeader->getChunkLength() - oversizedAmsduTrailer->getChunkLength()).get() == 70); + ASSERT(oversizedAmsdu->getByteLength() == 100); + + auto amsduFragmentationPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("oversizedAmsduFragmentationPolicy", this); + amsduFragmentationPolicyModule->par("fragmentationThreshold").setIntValue(90); + amsduFragmentationPolicyModule->callInitialize(); + auto amsduFragmentationPolicy = check_and_cast(amsduFragmentationPolicyModule); + ASSERT(oversizedAmsdu->getByteLength() > 90); + ASSERT(amsduFragmentationPolicy->computeFragmentSizes(oversizedAmsdu).empty()); + auto ordinaryQosData = makeQosDataPacket("ordinaryQosData", SequenceNumberCyclic(112), 70); + ASSERT(amsduFragmentationPolicy->computeFragmentSizes(ordinaryQosData) == std::vector({ 60, 10 })); + delete oversizedAmsdu; + delete ordinaryQosData; + amsduFragmentationPolicyModule->callFinish(); + amsduFragmentationPolicyModule->deleteModule(); + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("fragmentedAddbaDataService", this); recipientDataServiceModule->callInitialize(); auto recipientDataService = check_and_cast(recipientDataServiceModule); @@ -3934,6 +3972,75 @@ ASSERT(!dataService.hasEligibleFrame(&mixedQueue)); queue.packets.clear(); } +// IEEE Std 802.11-2024, 9.3.2.2.1-9.3.2.2.2 and Figure 9-123: only +// non-final Basic A-MSDU subframes carry 0-3 octets of alignment padding. +// The policy's size check must include that padding and accept the exact +// serialized limit. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + const int firstPayloadLengths[] = { 5, 4, 7 }; + const int expectedAmsduLengths[] = { 39, 39, 43 }; + for (int i = 0; i < 3; i++) { + TestPacketQueue queue; + auto first = makeAggregatePacket("paddingBoundaryFirst", SequenceNumberCyclic(15 + i * 2), firstPayloadLengths[i]); + auto second = makeAggregatePacket("paddingBoundarySecond", SequenceNumberCyclic(16 + i * 2), 5); + queue.packets = { first, second }; + TestBasicMsduAggregationPolicy policy; + policy.configure(B(expectedAmsduLengths[i] - 1)); + ASSERT(policy.computeAggregateFrames(&queue, first, [](const Packet *) { return true; }) == nullptr); + policy.configure(B(expectedAmsduLengths[i])); + auto selected = policy.computeAggregateFrames(&queue, first, [](const Packet *) { return true; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == first); + ASSERT(selected->at(1) == second); + MsduAggregation aggregation; + auto aggregated = aggregation.aggregateFrames(selected); + delete selected; + aggregated->popAtFront(); + aggregated->popAtBack(B(4)); + ASSERT(aggregated->getDataLength().get() == expectedAmsduLengths[i]); + // The expected length ends with the second 14-byte header and its + // 5-byte MSDU; any final padding would make this assertion fail. + const auto& finalSubframeHeader = aggregated->peekDataAt(B(expectedAmsduLengths[i] - 14 - 5), B(14)); + ASSERT(finalSubframeHeader != nullptr); + ASSERT(finalSubframeHeader->getLength() == 5); + delete aggregated; + queue.packets.clear(); + } + + // BasicMsduAggregationPolicy.ned documents -1 as an unlimited maximum. + TestPacketQueue unlimitedQueue; + auto unlimitedFirst = makeAggregatePacket("unlimitedPaddingFirst", SequenceNumberCyclic(21), 7); + auto unlimitedSecond = makeAggregatePacket("unlimitedPaddingSecond", SequenceNumberCyclic(22), 5); + unlimitedQueue.packets = { unlimitedFirst, unlimitedSecond }; + auto unlimitedPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.aggregation.BasicMsduAggregationPolicy")->create("unlimitedAmsduPolicy", this); + unlimitedPolicyModule->par("maxAMsduSize").setIntValue(-1); + unlimitedPolicyModule->callInitialize(); + auto unlimitedPolicy = check_and_cast(unlimitedPolicyModule); + auto unlimitedSelection = unlimitedPolicy->computeAggregateFrames(&unlimitedQueue, unlimitedFirst, [](const Packet *) { return true; }); + ASSERT(unlimitedSelection != nullptr); + ASSERT(unlimitedSelection->size() == 2); + MsduAggregation unlimitedAggregation; + auto unlimitedAggregate = unlimitedAggregation.aggregateFrames(unlimitedSelection); + delete unlimitedSelection; + unlimitedAggregate->popAtFront(); + unlimitedAggregate->popAtBack(B(4)); + ASSERT(unlimitedAggregate->getDataLength().get() == 43); + delete unlimitedAggregate; + unlimitedQueue.packets.clear(); + unlimitedPolicyModule->callFinish(); + unlimitedPolicyModule->deleteModule(); +} + // The built-in policy is anchored on the provider-selected candidate. Held // enumeration members do not hide later compatible frames, while an oversized // anchor falls back to ordinary dequeue without aggregating its followers. From 6034af19d3a6ed7d0186754d75f93c2aae866d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:52:46 +0200 Subject: [PATCH 41/91] ieee80211: fix: preserve management bodies during fragmentation Fragment bodies contain slices of the serialized management body, not repeated complete management headers. Preserve the body bytes through fragmentation and reassembly and keep transaction metadata with the fragments. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac.Fragmentation | behavior.change.fix | test whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 6 +++ .../mac/fragmentation/Fragmentation.cc | 14 ++--- tests/unit/Ieee80211MgmtTransactionTag_1.test | 51 +++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 9a003eb6082..933f87645a0 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -440,6 +440,12 @@ Notable backward incompatible changes are the following: transmissions and delivered traffic may change in configurations that previously built oversized or fragmentable A-MSDUs. +15. Serialized Management Fragment Bodies + + Management fragmentation now slices the serialized body instead of repeating a + complete management header in each fragment. Reassembly preserves the original body + bytes and local transaction context. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc index 4465c22ecf3..2f5897ebf0c 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc @@ -51,11 +51,12 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve const auto& frameHeader = frame->popAtFront(); frame->popAtBack(B(4)); const auto& actionFrame = dynamicPtrCast(frameHeader); - if (actionFrame != nullptr) { + const auto& managementHeader = dynamicPtrCast(frameHeader); + if (managementHeader != nullptr && managementHeader->getChunkLength() > makeShared()->getChunkLength()) { // IEEE Std 802.11-2024, 10.4: a fragment frame body carries only a - // portion of the MMPDU. Move the action body out of INET's combined + // portion of the MMPDU. Move the management body out of INET's combined // typed header before slicing it into fragment bodies. - Packet serializedHeader("serializedActionHeader", frameHeader); + Packet serializedHeader("serializedManagementHeader", frameHeader); const auto& headerBytes = serializedHeader.peekDataAsBytes()->getBytes(); auto bodyOffset = makeShared()->getChunkLength().get(); frame->insertAtFront(makeShared(std::vector(headerBytes.begin() + bodyOffset, headerBytes.end()))); @@ -75,15 +76,16 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve fragment->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset() + offset, fragment->getFrontOffset(), length); offset += length; Ptr fragmentHeader; + if (managementHeader != nullptr) + fragmentHeader = copyManagementHeader(managementHeader); + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); if (actionFrame != nullptr) { - fragmentHeader = copyManagementHeader(actionFrame); auto actionContext = staticPtrCast(actionFrame->dupShared()); actionContext->setFragmentNumber(i); actionContext->setMoreFragments(!lastFragment); fragment->addTag()->setActionFrame(actionContext); } - else - fragmentHeader = staticPtrCast(frameHeader->dupShared()); fragmentHeader->setSequenceNumber(frameHeader->getSequenceNumber()); fragmentHeader->setFragmentNumber(i); fragmentHeader->setMoreFragments(!lastFragment); diff --git a/tests/unit/Ieee80211MgmtTransactionTag_1.test b/tests/unit/Ieee80211MgmtTransactionTag_1.test index 568716711fc..90ff486609b 100644 --- a/tests/unit/Ieee80211MgmtTransactionTag_1.test +++ b/tests/unit/Ieee80211MgmtTransactionTag_1.test @@ -9,6 +9,9 @@ generic body region tag is clipped and rebased for each fragment. #include "inet/common/TimeTag_m.h" #include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" @@ -53,6 +56,54 @@ for (auto fragment : *fragments) delete fragment; delete fragments; +// Exercise the policy and fragmentation with an actual typed non-action response. +auto policyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("fragmentationPolicy", this); +policyModule->par("fragmentationThreshold").setIntValue(32); +policyModule->callInitialize(); +auto policy = check_and_cast(policyModule); +auto response = makeShared(); +response->setStatusCode(SC_SUCCESSFUL); +response->setAid(291); +Ieee80211SupportedRatesElement rates; +rates.numRates = 1; +rates.rate[0] = 6; +response->setSupportedRates(rates); +response->setChunkLength(B(9)); +auto responsePacket = new Packet("typedAssociationResponse", response); +auto responseHeader = makeShared(); +responseHeader->setType(ST_ASSOCIATIONRESPONSE); +responseHeader->setReceiverAddress(MacAddress("02:00:00:00:00:01")); +responseHeader->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); +responseHeader->setSequenceNumber(SequenceNumberCyclic(17)); +responsePacket->insertAtFront(responseHeader); +responsePacket->insertAtBack(makeShared()); +auto sizes = policy->computeFragmentSizes(responsePacket); +ASSERT(sizes == std::vector({4, 4, 1})); +auto responseFragments = fragmentation.fragmentFrame(responsePacket, sizes); +std::vector reassembledBody; +for (size_t i = 0; i < responseFragments->size(); i++) { + auto fragment = responseFragments->at(i); + ASSERT(fragment->getByteLength() <= 32); + ASSERT(fragment->findTag() == nullptr); + auto fragmentHeader = fragment->popAtFront(); + ASSERT(fragmentHeader->getChunkLength() == B(24)); + ASSERT(fragmentHeader->getType() == ST_ASSOCIATIONRESPONSE); + ASSERT(fragmentHeader->getReceiverAddress() == responseHeader->getReceiverAddress()); + ASSERT(fragmentHeader->getTransmitterAddress() == responseHeader->getTransmitterAddress()); + ASSERT(fragmentHeader->getSequenceNumber() == SequenceNumberCyclic(17)); + ASSERT(fragmentHeader->getFragmentNumber() == i); + ASSERT(fragmentHeader->getMoreFragments() == (i + 1 < responseFragments->size())); + fragment->popAtBack(B(4)); + auto bytes = fragment->peekDataAsBytes()->getBytes(); + ASSERT(bytes.size() == sizes[i]); + reassembledBody.insert(reassembledBody.end(), bytes.begin(), bytes.end()); + delete fragment; +} +ASSERT(reassembledBody == std::vector({0, 0, 0, 0, 0x23, 0xC1, 1, 1, 0x0C})); +delete responseFragments; +policyModule->callFinish(); +policyModule->deleteModule(); + EV << "Management transaction packet tag survived frame replacement.\n"; %contains: stdout From 6091e3268cdf9f7521d0aab772fd1c59349ea1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:53:54 +0200 Subject: [PATCH 42/91] ieee80211: fix: flag malformed peer association IDs Malformed peer association IDs are packet errors, not reasons to terminate the simulation. decodeAssociationId marks the header incorrect and the serializer regression checks the malformed cases. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mgmt.Ieee80211MgmtFrameSerializer | behavior.change.fix | test whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 5 +++ .../mgmt/Ieee80211MgmtFrameSerializer.cc | 16 ++++----- .../unit/Ieee80211MgmtFrameSerializer_1.test | 34 +++++++++++++------ 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 933f87645a0..b060b3ae7a2 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -446,6 +446,11 @@ Notable backward incompatible changes are the following: complete management header in each fragment. Reassembly preserves the original body bytes and local transaction context. +16. Malformed Peer Association IDs + + Malformed peer association IDs now mark the decoded management header incorrect + instead of terminating the simulation during deserialization. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 5fb46ae01ad..1b5a510e327 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -611,18 +611,18 @@ static uint16_t encodeAssociationId(Ieee80211StatusCode statusCode, int aid) return 0; } -static int decodeAssociationId(Ieee80211StatusCode statusCode, uint16_t wireAid) +static int decodeAssociationId(Ieee80211MgmtFrame& frame, Ieee80211StatusCode statusCode, 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"); const int aid = wireAid & ASSOCIATION_ID_MASK; - if (aid < 1 || aid > MAX_LOGICAL_ASSOCIATION_ID) - throw cRuntimeError("Malformed successful Association Response AID: %d", aid); + if ((wireAid & ASSOCIATION_ID_MARKER) != ASSOCIATION_ID_MARKER || aid < 1 || aid > MAX_LOGICAL_ASSOCIATION_ID) { + frame.markIncorrect(); + return 0; + } return aid; } if (wireAid != 0) - throw cRuntimeError("Malformed unsuccessful Association Response AID: expected zero, got 0x%04x", wireAid); + frame.markIncorrect(); return 0; } @@ -899,7 +899,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, frame->getStatusCode(), stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); @@ -911,7 +911,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, frame->getStatusCode(), stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); diff --git a/tests/unit/Ieee80211MgmtFrameSerializer_1.test b/tests/unit/Ieee80211MgmtFrameSerializer_1.test index 18e3638f495..a4790c32777 100644 --- a/tests/unit/Ieee80211MgmtFrameSerializer_1.test +++ b/tests/unit/Ieee80211MgmtFrameSerializer_1.test @@ -172,6 +172,27 @@ static void checkMalformedSupportedRates(const std::vector& prefix) ASSERT(zeroLength->getSupportedRates().numRates == 0); } +template +static void checkMalformedAssociationIds() +{ + for (uint16_t wireAid : {0x0001, 0x4001, 0x8001, 0xC000, 0xC7D8, 0xFFFF}) { + auto frame = deserializeMalformedBody({0, 0, 0, 0, + static_cast(wireAid), static_cast(wireAid >> 8), 1, 1, 0x0C}); + ASSERT(frame->isIncorrect()); + ASSERT(frame->isComplete()); + ASSERT(frame->getAid() == 0); + ASSERT(frame->getSupportedRates().numRates == 1); + } + for (uint16_t wireAid : {0x0001, 0xC000, 0xC001}) { + auto frame = deserializeMalformedBody({0, 0, 1, 0, + static_cast(wireAid), static_cast(wireAid >> 8), 1, 1, 0x0C}); + ASSERT(frame->isIncorrect()); + ASSERT(frame->isComplete()); + ASSERT(frame->getAid() == 0); + ASSERT(frame->getSupportedRates().numRates == 1); + } +} + %activity: { @@ -267,17 +288,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 9c732c354948c0cd50dcb36837142fcab52578f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 01:55:04 +0200 Subject: [PATCH 43/91] ieee80211: fix: retain negotiated A-MSDU support in Block Ack A successful ADDBA response supplies the recipient's A-MSDU support for this agreement. Retain that response bit rather than the request's initial capability so later aggregation obeys the negotiated agreement. Validation: diagnostic debug and release builds; 14 focused unit/module/queueing cases; 59 fingerprint ingredients with repeated moving candidates. Reconstructed commits receive the same applicable scoped checks before promotion. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: src.ieee80211.mac.OriginatorBlockAckAgreementHandler | behavior.change.fix | test whatsnew | pr-1148-resolve-audit-findings --- WHATSNEW | 6 ++++ .../OriginatorBlockAckAgreementHandler.cc | 1 + tests/unit/Ieee80211AddbaTransaction_1.test | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/WHATSNEW b/WHATSNEW index b060b3ae7a2..ef6292c6d47 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -451,6 +451,12 @@ Notable backward incompatible changes are the following: Malformed peer association IDs now mark the decoded management header incorrect instead of terminating the simulation during deserialization. +17. Negotiated A-MSDU Support + + An originator now retains the successful ADDBA response's A-MSDU support bit for the + agreement. Subsequent aggregation follows the negotiated response instead of the + request's initial capability. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index c830853cc34..32764c96f8f 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -317,6 +317,7 @@ OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processR void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp) { agreement->setIsAddbaResponseReceived(true); + agreement->setIsAMsduSupported(addbaResp->getAMsduSupported()); agreement->setBufferSize(addbaResp->getBufferSize()); agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue()); agreement->calculateExpirationTime(); diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 07eed429a4c..87e4a03544c 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -1090,6 +1090,35 @@ assertMaxReceiveLifetimeValidation("inet.linklayer.ieee80211.mac.recipient.Recip ASSERT(request1->getDialogToken() == 1); } +// IEEE Std 802.11-2024, 9.4.1.13: the recipient's A-MSDU support controls +// whether an A-MSDU can use Block Ack under the established agreement. +for (bool responseSupportsAMsdu : { false, true }) { + TestOriginatorHandler capabilityHandler; + TestCallback capabilityCallback; + Packet capabilityTrigger("capabilityTrigger"); + auto capabilityHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(100)); + capabilityHandler.processAcknowledgedDataFrame(&capabilityTrigger, capabilityHeader, &originatorPolicy, &capabilityCallback); + auto request = dynamicPtrCast(capabilityCallback.managementHeader); + ASSERT(request != nullptr && request->getAMsduSupported()); + capabilityHandler.processTransmittedAddbaReq(capabilityCallback.managementPacket, request, &originatorPolicy, &capabilityCallback); + auto response = makeResponse(peer1, 3, request->getDialogToken(), 0); + response->setAMsduSupported(responseSupportsAMsdu); + auto outcome = capabilityHandler.processReceivedAddbaResp(response, &originatorPolicy, &capabilityCallback); + auto agreement = outcome.establishedAgreement; + ASSERT(agreement != nullptr); + ASSERT(agreement->getIsAMsduSupported() == responseSupportsAMsdu); + + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.configureBlockAckSelection(1000, 1); + for (bool aMsduPresent : { false, true }) { + auto header = makeQosHeader(peer1, 3, request->getStartingSequenceNumber()); + header->setAMsduPresent(aMsduPresent); + Packet packet("capabilityData", header); + ASSERT(ackPolicy.computeAckPolicy(&packet, header, agreement) == + (aMsduPresent && !responseSupportsAMsdu ? NORMAL_ACK : BLOCK_ACK)); + } +} + TestOriginatorHandler handler; Packet triggerPacket("trigger"); auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); From e920fcf908397a289b69b6e990aeef5de1c6dedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 02:00:47 +0200 Subject: [PATCH 44/91] plan: change: record completed PR1148 verification Record the accepted baseline provenance, passing source-prefix and final verification, repaired findings, preserved checkpoints and scoped evidence limitations. Plan: plan/pending/pr-1148-resolve-audit-findings.md Change: plan | behavior.change | - | pr-1148-resolve-audit-findings --- .../pending/pr-1148-resolve-audit-findings.md | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/plan/pending/pr-1148-resolve-audit-findings.md b/plan/pending/pr-1148-resolve-audit-findings.md index 39f816f6b0f..a3a9128cf44 100644 --- a/plan/pending/pr-1148-resolve-audit-findings.md +++ b/plan/pending/pr-1148-resolve-audit-findings.md @@ -1,8 +1,7 @@ # Resolve the audit findings of PR #1148 -Status: **approved reconstruction in progress** — F1/F2 and interface repairs, isolated -rebase, and all 17 diagnostic source-prefix checks are complete. Final history -authoring and verification are in progress after explicit acceptance of the exact baseline and history proposals. +Status: **complete** — the repairs, approved reconstruction, per-commit checks, +feature builds, final union and middle-commit checks are complete. Prepared: 2026-09-14. PR: [INET #1148](https://github.com/inet-framework/inet/pull/1148). Audit evidence: local, ignored `audit/pull-request/pr-1148.md`, @@ -120,7 +119,7 @@ Tests and acceptance: pre-existing ledgered `ITransmitStep`/`IReceiveStep` defaults outside this repair. - [x] S5: assign `DEQUEUED=0`, `REMOVED=1`, `DROPPED=2` explicitly in `src/inet/queueing/contract/IPacketQueue.h`; preserve those values in future changes. -- [ ] Compile affected implementations with the relevant optional features both enabled and +- [x] Compile affected implementations with the relevant optional features both enabled and disabled. Run the interface scan and manually inspect nested callbacks, which the audit's scanner did not detect. @@ -151,25 +150,25 @@ messages. The final commit count follows the resulting decisions, not a target o - [x] Build a fresh debug library and run the focused tests on the repaired original-base tree. Preserve that tested checkpoint before rebasing. -- [ ] Pin the then-current upstream target. The audit observed a merge conflict against +- [x] Pin the then-current upstream target. The audit observed a merge conflict against `0c85e5dd6bc2969987286210962983b19384670e`; do not assume that is still the target. Resolve the rebase in an isolated candidate, using the INET rebase workflow and scoped `opp_repl` evidence. Record conflict decisions and rerun affected tests after resolution. -- [ ] Inventory the 16 fingerprint rows added/changed by the audited series, then recompute the +- [x] Inventory the 16 fingerprint rows added/changed by the audited series, then recompute the inventory against the pinned new base. Map changed production paths through dependency data to affected configurations, runs and seeds; include all declared ingredients (`tplx`, `~tNl`, `~tND` where applicable). -- [ ] For each moving row, record configuration/run, old and candidate values, ingredient, +- [x] For each moving row, record configuration/run, old and candidate values, ingredient, causal source commit, observed behavioral difference, correctness argument and artifact paths. Multiple transitions of the same row need evidence at each responsible commit. -- [ ] Investigate mismatches before replacing expectations. Present the exact proposed baseline +- [x] Investigate mismatches before replacing expectations. Present the exact proposed baseline changes for acceptance under [change-a-baseline.md](../../doc/project/guide/change-a-baseline.md). Candidate calculations remain evidence until the required approval is recorded. -- [ ] Reconstruct the final series on the fixed new base using the INET branch-cleanup workflow. +- [x] Reconstruct the final series on the fixed new base using the INET branch-cleanup workflow. Split C12, fold each accepted baseline transition into its causal commit, remove standalone C14, restore S6's incidental whitespace and complete S3's trailers. Do not squash all baseline changes into C13 merely because it precedes C14. -- [ ] Verify each final intermediate tree with its directly applicable build/tests and scoped +- [x] Verify each final intermediate tree with its directly applicable build/tests and scoped fingerprints. Compare the reconstructed final source tree with the tested post-rebase tree; account explicitly for intended whitespace, documentation and baseline differences. @@ -183,28 +182,28 @@ inet_run_module_tests -m debug -f '(Ieee80211BlockAckInactivityTimer_1|Ieee80211 inet_run_queueing_tests -m debug -f 'PacketQueueDepartureSignal_1\.test$' ``` -- [ ] Add explicit filters for the new regressions and retain commands, mode, seeds, exit status +- [x] Add explicit filters for the new regressions and retain commands, mode, seeds, exit status and logs. Repeat tests after relevant changes; do not reuse pre-rebase results as final evidence. -- [ ] Complete debug and release compilation and the affected feature matrix using the supported +- [x] Complete debug and release compilation and the affected feature matrix using the supported build workflow. Run source-seal, architecture, interface, naming, classification, commit and whitespace gates as described in [run-the-gates.md](../../doc/project/guide/run-the-gates.md). -- [ ] Resolve the audit's usage/override inventory and affected standards-traceability gaps. +- [x] Resolve the audit's usage/override inventory and affected standards-traceability gaps. Inspect authoritative local clauses for claims this series makes; record unavailable evidence explicitly rather than marking it passed. Keep pre-existing ledgered findings separate. -- [ ] Refresh the audit and commit summary against the final exact head/base. Close F1/F2 only +- [x] Refresh the audit and commit summary against the final exact head/base. Close F1/F2 only with the production-path tests above; close S1 only with per-row and per-commit evidence. -- [ ] Correct the PR description: malformed AID handling is +- [x] Correct the PR description: malformed AID handling is `Ieee80211MgmtFrameSerializer::decodeAssociationId` calling `markIncorrect`; the teardown tag carries `generationId`, while role/peer/TID come from DELBA/context. Include actual reading order, architectural/API surface, baseline causes and verified test scope. -- [ ] Hand off the final diff, revised description and evidence table for review. Move the plan +- [x] Hand off the final diff, revised description and evidence table for review. Move the plan to `plan/done/` only after its required repairs and verification are complete. Completion means both reproduced defects are covered and repaired, all six policy findings are resolved, the series builds and passes its applicable tests at every commit, and the final audit clearly distinguishes verified claims from any remaining evidence limitations. -## Execution checkpoint — 2026-09-15 +## Historical execution checkpoint — 2026-09-15 Reviewed head preserved at `archive/pr-1148-reviewed-991f626a4f`. The original-base repair checkpoint is `d3045c9780`, preserved at @@ -241,7 +240,7 @@ intact. No new fingerprint expectation has been accepted or written. S1, final C12 split, S3 classification, S6 restoration, per-commit verification, and final review handoff remain open. Do not move this plan to done at this checkpoint. -## Diagnostic checkpoint — 2026-09-15 01:25 +## Historical diagnostic checkpoint — 2026-09-15 01:25 All 17 source prefixes build in debug and release and pass their available focused cases (202 case executions). All 994 declared fingerprint ingredients @@ -260,3 +259,37 @@ Apply inherited upstream graphical drift in a prerequisite baseline commit, then this plan, then the 17 measured source decisions with their causal expectations and migration notes, and finally the verified closure. Preserve the original topic and all diagnostic checkpoints. + +## Final verification — 2026-09-15 + +The user accepted the exact baseline and history proposals. The reconstructed +source series is verified through `ab2531bf32c20fe6fd7efb0299ce8ea00058907e`, on pinned +base `0c85e5dd6bc2969987286210962983b19384670e`. The preserved original topic remains +`37c6119f7ee6a090b2aa0999d1d9ffc3dca94144`. + +All 19 prerequisite/plan/source commits have passing debug/release and scoped +verification. The final union passes five unit, six module and three queueing +cases plus 59 fingerprint ingredients across 16 mapped rows. Middle checks +cover C03 transactions/RED, C08 reentrant expiry/HCF overflow and C13 A-MSDU +validity. Queueing-only and Wi-Fi-required debug builds pass. + +Every source-owned baseline transition is in its causal commit; the ten +inherited graphical tokens are in the approved upstream-control prerequisite. +C12 is split, C14 is removed, C17 and the whole series have classification +trailers, incidental whitespace is restored, and new public contracts have +migration-guide and release notes. The final source matches the tested rebase +except for the planned whitespace and three verified figure-reference comments. + +Full-tree architecture/interface/naming gates retain the same 25/15/23 findings +as pinned upstream, with no new findings. Scoped architecture, queue interfaces, +changed NED/MSG declarations, source seals, history and whitespace checks pass. +The two existing Wi-Fi interface defaults remain ledgered by AV-CONTRACT-02. +The source-based usage inventory replaces the unavailable historical helper; +external implementations and every optional IEEE procedure are not certified. + +The completion record and its pure move are separate under PR-SPLIT-MOVE, so +the final series contains 21 commits without changing the accepted source +order or baseline scope. Local final audit, commit summary, revised PR +description, exact commands/results and tree proof are under ignored +`audit/pull-request/pr-1148-repair/`; the original audit remains preserved. +No remote publication is part of this execution. From 357f43d35c3342a8bc13541d2badf7dd86cc8b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 02:01:15 +0200 Subject: [PATCH 45/91] plan: location: archive the completed PR1148 plan Move the completed plan without changing its content, preserving its history and the already verified source tree. Plan: plan/done/pr-1148-resolve-audit-findings.md Change: plan | location | - | pr-1148-resolve-audit-findings --- plan/{pending => done}/pr-1148-resolve-audit-findings.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plan/{pending => done}/pr-1148-resolve-audit-findings.md (100%) diff --git a/plan/pending/pr-1148-resolve-audit-findings.md b/plan/done/pr-1148-resolve-audit-findings.md similarity index 100% rename from plan/pending/pr-1148-resolve-audit-findings.md rename to plan/done/pr-1148-resolve-audit-findings.md From db0cb19e9be3377f5bf161612e217d728294b0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 02:27:40 +0200 Subject: [PATCH 46/91] ieee80211: fix: isolate Block Ack request state by receiver With outstanding frames for two peers sharing a TID and sequence number, a BAR addressed to one peer marked both peers as waiting for Block Ack. The other peer then remained ineligible for retransmission or its own BAR, because response and timeout processing only updated the addressed peer. Filter ACK-status entries by the BAR receiver before applying the existing TID and sequence checks. Add a regression covering Basic and Compressed BARs with both response and timeout outcomes. Validation: debug build; peer-isolation regression fails before the fix and passes after it; existing ADDBA transaction test passes; both focused QoS Block Ack fingerprint cases pass with unchanged baselines. Scoped source seal and architecture checks pass. Change: src.ieee80211.mac.QosAckHandler | behavior.change.fix | test --- .../ieee80211/mac/originator/QosAckHandler.cc | 2 + .../Ieee80211BlockAckPeerIsolation_1.test | 85 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/unit/Ieee80211BlockAckPeerIsolation_1.test diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index d764a2c2433..b808f44fc96 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -213,6 +213,8 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const Ptr& blockAckReq) { for (auto& ackStatus : ackStatuses) { + if (ackStatus.first.first != blockAckReq->getReceiverAddress()) + continue; auto tid = ackStatus.first.second.first; auto seqCtrlField = ackStatus.first.second.second; auto& status = ackStatus.second; diff --git a/tests/unit/Ieee80211BlockAckPeerIsolation_1.test b/tests/unit/Ieee80211BlockAckPeerIsolation_1.test new file mode 100644 index 00000000000..d2e8f1ca6a9 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckPeerIsolation_1.test @@ -0,0 +1,85 @@ +%description: +Basic and Compressed BARs change ACK state only for their addressed peer. +Another peer with the same TID and sequence number remains outstanding after +either a response or a timeout, and can subsequently request its own Block Ack. + +%includes: +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +%activity: +const MacAddress peerA("02:00:00:00:00:01"); +const MacAddress peerB("02:00:00:00:00:02"); +for (bool compressed : { false, true }) { + for (bool timeout : { false, true }) { + QosAckHandler handler; + auto makeData = [](MacAddress peer) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(4); + header->setSequenceNumber(SequenceNumberCyclic(100)); + header->setAckPolicy(BLOCK_ACK); + return header; + }; + auto dataA = makeData(peerA); + auto dataB = makeData(peerB); + handler.processTransmittedDataOrMgmtFrame(dataA); + handler.processTransmittedDataOrMgmtFrame(dataB); + auto makeBar = [compressed](MacAddress peer) -> Ptr { + if (compressed) { + auto bar = makeShared(); + bar->setReceiverAddress(peer); + bar->setTidInfo(4); + bar->setStartingSequenceNumber(SequenceNumberCyclic(100)); + return bar; + } + auto bar = makeShared(); + bar->setReceiverAddress(peer); + bar->setTidInfo(4); + bar->setStartingSequenceNumber(SequenceNumberCyclic(100)); + return bar; + }; + auto barA = makeBar(peerA); + handler.processTransmittedBlockAckReq(barA); + ASSERT(handler.getQoSDataAckStatus(dataA) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(handler.getQoSDataAckStatus(dataB) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.isOutstandingFrame(dataB)); + if (timeout) + handler.processFailedBlockAckReq(barA); + else if (compressed) { + auto response = makeShared(); + response->setTransmitterAddress(peerA); + response->setTidInfo(4); + response->setStartingSequenceNumber(SequenceNumberCyclic(100)); + response->setBlockAckBitmap(BitVector(std::vector(8, 0))); + handler.processReceivedBlockAck(response); + } + else { + auto response = makeShared(); + response->setTransmitterAddress(peerA); + response->setTidInfo(4); + response->setStartingSequenceNumber(SequenceNumberCyclic(100)); + for (int i = 0; i < 64; i++) + response->setBlockAckBitmap(i, BitVector(0, 16)); + handler.processReceivedBlockAck(response); + } + ASSERT(handler.getQoSDataAckStatus(dataA) == (timeout ? QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED : QosAckHandler::Status::BLOCK_ACK_ARRIVED_UNACKED)); + ASSERT(handler.isEligibleToTransmit(dataA)); + ASSERT(handler.getQoSDataAckStatus(dataB) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.isOutstandingFrame(dataB)); + auto barB = makeBar(peerB); + handler.processTransmittedBlockAckReq(barB); + ASSERT(handler.getQoSDataAckStatus(dataB) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + handler.processFailedBlockAckReq(barB); + ASSERT(handler.isEligibleToTransmit(dataB)); + } +} + +EV << "Block Ack peer isolation checks passed.\n"; + +%contains: stdout +Block Ack peer isolation checks passed. From a7352e1344c664c8894d656b9e64f6f89de7ac38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 21 Sep 2026 14:31:20 +0200 Subject: [PATCH 47/91] tests: fix: adapt cancellation regression to MIB ownership API The cancellation regression introduced by this topic accessed MIB fields that the new upstream makes private. Use its owner API so the test builds on the rebased branch. Continue release-note numbering after the upstream additions. Change: tests | behavior.change.fix | whatsnew --- WHATSNEW | 26 +++++++++---------- .../module/Ieee80211MgmtApCancellation_1.test | 6 ++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index ef6292c6d47..a7a131514e8 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -333,7 +333,7 @@ Notable backward incompatible changes are the following: nested compound queues exactly once. PacketBuffer rejects packets owned by a cPacketQueue whose owner cannot participate in the buffer callback contract. -5. Block Ack DELBA agreement ownership +18. Block Ack DELBA agreement ownership Custom IBlockAckAgreementHandlerCallback implementations must explicitly implement releaseBlockAckAgreementFrames() and cancelBlockAckTeardown(). @@ -380,7 +380,7 @@ Notable backward incompatible changes are the following: IOriginatorBlockAckAgreementPolicy must replace computeAddbaFailureTimeout() with getAddbaResponseTimeout() and implement computeAddbaRetryBackoff(). -6. Originator ADDBA Transactions and Eligible Frames +19. Originator ADDBA Transactions and Eligible Frames ADDBA response deadlines now begin at actual request transmission. Frames for a pending peer/TID wait for a matching successful response or terminal failure, then @@ -388,7 +388,7 @@ Notable backward incompatible changes are the following: trajectories. A-MSDU service now appears as dequeued traffic in queue counters and graphical fingerprints, instead of administrative removal. -7. Fragment Reassembly and Receive Lifetime +20. Fragment Reassembly and Receive Lifetime Fragmented management actions are dispatched only after their complete body is reassembled. Shared fragment reassembly now expires incomplete bodies after @@ -396,62 +396,62 @@ Notable backward incompatible changes are the following: instead of completing an old body; affected application deliveries, forwarding trajectories and fingerprints change. -8. Block Ack Inactivity Timer Callbacks +21. Block Ack Inactivity Timer Callbacks Block Ack inactivity uses absolute per-agreement deadlines. The shared timer follows the earliest enabled deadline and is cancelled when none remains. Agreement expiry is safe when a bounded management queue drops a generated DELBA synchronously. Timers and traffic may differ from runs that kept extending an inactivity interval. -9. Fragment Identity Across Sequence Reuse +22. Fragment Identity Across Sequence Reuse Receive reassembly separates fragment generations and sequence spaces. Late fragments, retries and reused sequence numbers no longer combine bodies from unrelated transmissions; affected receive drops and deliveries may change. -10. Receive Lifetime in Block Ack Reordering +23. Receive Lifetime in Block Ack Reordering Receive lifetime now remains anchored to first reception while fragments wait in Block Ack reordering. A later stage no longer gives an old incomplete body a fresh lifetime, so delayed or incomplete bodies may be dropped earlier. -11. Expired Agreement Quarantine and Frame Release +24. Expired Agreement Quarantine and Frame Release Expired Block Ack agreements are quarantined from data-plane use while teardown state is retained. Outstanding frames are released through the normal acknowledgment/recovery paths before teardown proceeds, including when callbacks replace agreement state. -12. Incomplete Data Released by a Block Ack Request +25. Incomplete Data Released by a Block Ack Request When a BAR releases data fragments that do not reassemble into a packet, recipient processing skips data dispatch instead of dereferencing a null result. -13. Recipient Block Ack Timeout Policy +26. Recipient Block Ack Timeout Policy Recipient Block Ack timeout selection now honors a nonzero local policy value; zero inherits the request's advisory value. This corrects the previously reversed choice and can change agreement expiry and subsequent traffic. -14. A-MSDU Size and Fragmentation Limits +27. A-MSDU Size and Fragmentation Limits Basic A-MSDUs now include subframe headers and padding in size checks and avoid the ordinary fragmentation path. Eligible members retain flow order. Aggregation, transmissions and delivered traffic may change in configurations that previously built oversized or fragmentable A-MSDUs. -15. Serialized Management Fragment Bodies +28. Serialized Management Fragment Bodies Management fragmentation now slices the serialized body instead of repeating a complete management header in each fragment. Reassembly preserves the original body bytes and local transaction context. -16. Malformed Peer Association IDs +29. Malformed Peer Association IDs Malformed peer association IDs now mark the decoded management header incorrect instead of terminating the simulation during deserialization. -17. Negotiated A-MSDU Support +30. Negotiated A-MSDU Support An originator now retains the successful ADDBA response's A-MSDU support bit for the agreement. Subsequent aggregation follows the negotiated response instead of the diff --git a/tests/module/Ieee80211MgmtApCancellation_1.test b/tests/module/Ieee80211MgmtApCancellation_1.test index 19da98e10df..d9df4e454b6 100644 --- a/tests/module/Ieee80211MgmtApCancellation_1.test +++ b/tests/module/Ieee80211MgmtApCancellation_1.test @@ -214,7 +214,7 @@ class TestIeee80211MgmtApCancellation : public Ieee80211MgmtAp auto& sta = staList[address]; sta.address = address; sta.authSeqExpected = 1; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + mib->setPeerAssociationStatus(address, Ieee80211Mib::AUTHENTICATED); } void submitAssociationRequest(const MacAddress& address) @@ -280,12 +280,12 @@ class TestIeee80211MgmtApCancellation : public Ieee80211MgmtAp void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } 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->getPeerAssociationStatus(address); } protected: From b89620244d5a93175ed5c72ed62a226c5f952509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 01:48:24 +0200 Subject: [PATCH 48/91] feature(physicallayer): add HT Greenfield mode set and PHY transmission support Register the n(greenfield-2.4Ghz) mode set and distinguish selectable operational modes (containsMode) from supported PHY capabilities (supportsMode). HT Greenfield profiles now explicitly support non-HT and HT-mixed response modes without making them selectable for data transmissions. Precompute immutable control-response mappings in Ieee80211ModeSet and make the mode-set registry thread-local to ensure thread safety. In the physical layer, validate per-packet transmission requests and reception feasibility against supported mode capabilities. Decompose transmission duration into preamble, header, and data intervals, correctly accounting for HT/VHT SIG field integration in the preamble. Publish modesetChangedSignal upon radio mode set transitions. Add unit test coverage for HT Greenfield compliant modes, duration decomposition, transmitter transition invariants, and ERP mode isolation. --- .../ieee80211/mode/Ieee80211HtMode.cc | 2 +- .../ieee80211/mode/Ieee80211ModeSet.cc | 439 +++++++++++++++++- .../ieee80211/mode/Ieee80211ModeSet.h | 36 +- .../ieee80211/mode/Ieee80211VhtMode.cc | 6 +- .../ieee80211/packetlevel/Ieee80211Radio.cc | 17 +- .../ieee80211/packetlevel/Ieee80211Radio.ned | 2 +- .../packetlevel/Ieee80211Receiver.cc | 4 +- .../packetlevel/Ieee80211Receiver.ned | 3 +- .../packetlevel/Ieee80211Transmitter.cc | 17 +- .../packetlevel/Ieee80211Transmitter.ned | 2 +- tests/unit/Ieee80211HtGreenfield_1.test | 354 ++++++++++++++ tests/unit/Ieee80211HtGuardInterval_1.test | 3 +- 12 files changed, 848 insertions(+), 37 deletions(-) create mode 100644 tests/unit/Ieee80211HtGreenfield_1.test diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index f74a0a7433b..fb630c9e84d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -380,7 +380,7 @@ const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee802 const Ieee80211HtDataMode *dataMode = new Ieee80211HtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211HtPreambleMode *preambleMode = new Ieee80211HtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211HtMode *htMode = new Ieee80211HtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); + singleton.modeCache.emplace(htModeId, htMode); return htMode; } return mode->second; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index e27e289f704..426ef2a2a68 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -9,6 +9,8 @@ #include #include +#include +#include #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" @@ -25,7 +27,147 @@ namespace physicallayer { Register_Abstract_Class(Ieee80211ModeSet); -const DelayedInitializer> Ieee80211ModeSet::modeSets([]() { return new std::vector { +std::vector Ieee80211ModeSet::completeHtGuardIntervalVariants(const char *name, const std::vector& entries) +{ + if (strcmp(name, "n(mixed-2.4Ghz)") && strcmp(name, "n(greenfield-2.4Ghz)")) + return entries; + + std::vector completeEntries = entries; + // IEEE Std 802.11-2024, Table 19-6 defines the 800 ns and 400 ns GIs. + // Add only the alternate GI for each mode explicitly declared above; + // declaration order, mandatory flags, and the historical catalog remain + // authoritative for this operation mode. + auto numberOfBaseEntries = completeEntries.size(); + for (size_t index = 0; index < numberOfBaseEntries; index++) { + auto htMode = dynamic_cast(completeEntries[index].mode); + if (htMode == nullptr) + continue; + auto dataMode = htMode->getDataMode(); + auto alternateGuardInterval = dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG ? + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT : Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG; + auto alternateMode = Ieee80211HtCompliantModes::getCompliantMode( + dataMode->getModulationAndCodingScheme(), htMode->getCenterFrequencyMode(), + htMode->getPreambleMode()->getPreambleFormat(), alternateGuardInterval); + auto alternateAlreadyPresent = std::any_of(completeEntries.begin(), completeEntries.end(), [alternateMode](const Entry& entry) { return entry.mode == alternateMode; }); + if (!alternateAlreadyPresent) + completeEntries.push_back({false, alternateMode}); + } + return completeEntries; +} + +#define HT_MODE_ENTRY(WIDTH, MCS, MANDATORY, FORMAT, GUARD_INTERVAL) \ + { MANDATORY, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs##MCS##BW##WIDTH##MHz, Ieee80211HtMode::BAND_2_4GHZ, FORMAT, GUARD_INTERVAL) }, +#define HT_MODE_ENTRIES_20(FORMAT) \ + HT_MODE_ENTRY(20, 0, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 1, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 2, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 3, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 4, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 5, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 6, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 7, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 8, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 9, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 10, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 11, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 12, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 13, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 14, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 15, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 16, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 17, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 18, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 19, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 20, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 21, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 22, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 23, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 24, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 25, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 26, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 27, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 28, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 29, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 30, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 31, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) +#define HT_MODE_ENTRIES_40(FORMAT) \ + HT_MODE_ENTRY(40, 0, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 1, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 2, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 3, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 4, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 5, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 6, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 7, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 8, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 9, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 10, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 11, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 12, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 13, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 14, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 15, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 16, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 17, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 18, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 19, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 20, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 21, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 22, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 23, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 24, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 25, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 26, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 27, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 28, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 29, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 30, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 31, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + +static std::vector createHtEntries(Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat) +{ + return { + HT_MODE_ENTRIES_20(preambleFormat) + HT_MODE_ENTRIES_40(preambleFormat) + }; +} + +static std::vector createHtSupportedEntries(Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat) +{ + auto result = createHtEntries(preambleFormat); + if (preambleFormat == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD) { + auto mixedEntries = createHtEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); + result.insert(result.end(), mixedEntries.begin(), mixedEntries.end()); + } + // Every 2.4 GHz HT STA supports the mandatory Clause 16/18 modes, and a + // Greenfield STA additionally supports HT-mixed PPDUs (IEEE 802.11-2024 + // 19.1.1 and 19.1.4). These are supplementary capabilities rather than + // selectable operating modes, so they are kept out of createHtEntries(). + result.insert(result.end(), { + { true, &Ieee80211DsssCompliantModes::dsssMode1Mbps, true }, + { true, &Ieee80211DsssCompliantModes::dsssMode2Mbps, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode9Mbps }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode18Mbps }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode36Mbps }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode48Mbps }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode54Mbps }, + }); + return result; +} + +#undef HT_MODE_ENTRIES_40 +#undef HT_MODE_ENTRIES_20 +#undef HT_MODE_ENTRY + +OPP_THREAD_LOCAL const DelayedInitializer> Ieee80211ModeSet::modeSets([]() { return new std::vector { Ieee80211ModeSet("a", { { true, &Ieee80211OfdmCompliantModes::ofdmMode6MbpsCS20MHz, true }, { false, &Ieee80211OfdmCompliantModes::ofdmMode9MbpsCS20MHz, true }, @@ -220,7 +362,73 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, true }, { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, true }, { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, true } - }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true), + }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true, createHtSupportedEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED)), + Ieee80211ModeSet("n(greenfield-2.4Ghz)", { + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs1BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs2BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs3BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs4BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs5BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs6BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs7BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs8BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs9BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs10BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs11BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs12BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs13BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs14BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs15BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs16BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs17BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs18BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs19BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs20BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs21BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs22BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs23BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs24BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs25BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs26BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs27BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs28BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs29BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs30BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs31BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs1BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs2BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs3BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs4BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs5BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs6BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs7BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs8BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs9BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs10BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs11BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs12BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs13BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs14BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs15BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs16BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs17BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs18BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs19BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs20BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs21BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs22BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs23BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs24BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs25BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs26BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs27BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs28BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs29BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs30BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs31BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) } + }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true, createHtSupportedEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD)), Ieee80211ModeSet("ac", []() { std::vector entries { { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, @@ -561,12 +769,16 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::VHT),}; }); Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector entries, const IIeee80211Mode *referenceMode, - PhyType phyType, bool htOperationSupported) : + PhyType phyType, bool htOperationSupported, const std::vector supportedEntries) : name(name), - entries(entries), + entries(completeHtGuardIntervalVariants(name, entries)), phyType(phyType), referenceMode(referenceMode), - htOperationSupported(htOperationSupported) + htOperationSupported(htOperationSupported), + supportedEntries(supportedEntries.empty() ? this->entries : completeHtGuardIntervalVariants(name, supportedEntries)), + controlResponseModes(createControlResponseModes(this->supportedEntries)), + htMixedControlResponseModes(createHtMixedControlResponseModes(this->supportedEntries)), + nonHtControlResponseEntries(createNonHtControlResponseEntries(this->supportedEntries)) { if (this->entries.empty()) throw cRuntimeError("IEEE 802.11 mode set '%s' must contain at least one mode", this->name.c_str()); @@ -606,7 +818,7 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en // 9.4.2.3 and 11.1.4.6); management splits it across the primary and // Extended Supported Rates elements when necessary. for (bool mandatory : {true, false}) { - for (const auto& entry : this->entries) { + for (const auto& entry : this->supportedEntries) { if (entry.isLegacyOperational && entry.isMandatory == mandatory) this->legacyOperationalModes.push_back(entry.mode); } @@ -619,10 +831,14 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en for (const auto *mode : this->legacyOperationalModes) { if (mode == nullptr) throw cRuntimeError("IEEE 802.11 mode set '%s' contains a null legacy operational mode", this->name.c_str()); - int modeIndex = findModeIndex(mode); - if (modeIndex < 0) - throw cRuntimeError("Legacy operational mode '%s' is not contained in IEEE 802.11 mode set '%s'", mode->getName(), this->name.c_str()); - if (!this->entries[modeIndex].isLegacyOperational) + bool found = false; + for (const auto& entry : this->supportedEntries) { + if (entry.mode == mode && entry.isLegacyOperational) { + found = true; + break; + } + } + if (!found) throw cRuntimeError("Legacy operational mode '%s' is not marked eligible in IEEE 802.11 mode set '%s'", mode->getName(), this->name.c_str()); } for (const auto& entry : this->entries) { @@ -670,6 +886,109 @@ int Ieee80211ModeSet::findModeIndex(const IIeee80211Mode *mode) const return -1; } +std::map Ieee80211ModeSet::createControlResponseModes(const std::vector& supportedEntries) +{ + std::map result; + // IEEE 802.11-2024 Table 9-230 defines the Basic HT-MCS Set as a BSS- + // configured bitmap of MCS indexes. It is not modelled here, so use the + // mandatory entries (currently the 20 MHz entries) as a bounded fallback + // for the candidate indexes. Clause 10.6.6.5.3 selects CH_BANDWIDTH + // separately, and the candidate filter below then keeps only modes at the + // source bandwidth. Consequently, optional 40 MHz MCS 0..7 are treated as + // candidate MCSs. A modelled Basic HT-MCS Set would replace this mandatory- + // index fallback with the BSS-configured indexes; bandwidth filtering would + // remain a separate step. + std::set mandatoryHtMcsIndexes; + for (const auto& entry : supportedEntries) { + auto mode = dynamic_cast(entry.mode); + if (entry.isMandatory && mode != nullptr) + mandatoryHtMcsIndexes.insert(mode->getDataMode()->getMcsIndex()); + } + for (const auto& sourceEntry : supportedEntries) { + auto source = dynamic_cast(sourceEntry.mode); + if (source == nullptr) + continue; + std::vector candidates; + for (const auto& candidateEntry : supportedEntries) { + auto candidate = dynamic_cast(candidateEntry.mode); + if (candidate != nullptr && candidate->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + candidate->getCenterFrequencyMode() == source->getCenterFrequencyMode() && + candidate->getDataMode()->getBandwidth() == source->getDataMode()->getBandwidth() && + mandatoryHtMcsIndexes.find(candidate->getDataMode()->getMcsIndex()) != mandatoryHtMcsIndexes.end() && + candidate->getDataMode()->getMcsIndex() <= source->getDataMode()->getMcsIndex() && + candidate->getDataMode()->getNumberOfSpatialStreams() <= source->getDataMode()->getNumberOfSpatialStreams()) + candidates.push_back(candidate); + } + // IEEE 802.11-2024 10.6.6.5.3: with no Basic HT-MCS Set modelled, + // CandidateMCSSet uses the mandatory-index fallback. After the + // bandwidth and MCS-index bounds, + // retain the highest NSS not exceeding the received NSS, then select the + // highest indexed MCS whose per-stream modulation and coding rate do not + // exceed those of the received MCS. The modelled MCS 0..31 are EQM. + int highestNss = -1; + for (auto candidate : candidates) + highestNss = std::max(highestNss, candidate->getDataMode()->getNumberOfSpatialStreams()); + const Ieee80211HtMode *response = nullptr; + auto sourceMcs = source->getDataMode()->getModulationAndCodingScheme(); + int sourceModulation = sourceMcs->getModulation()->getSubcarrierModulation()->getCodeWordSize(); + double sourceCodeRate = sourceMcs->getCode()->getForwardErrorCorrection()->getCodeRate(); + for (auto candidate : candidates) { + auto candidateDataMode = candidate->getDataMode(); + auto candidateMcs = candidateDataMode->getModulationAndCodingScheme(); + if (candidateDataMode->getNumberOfSpatialStreams() == highestNss && + candidateMcs->getModulation()->getSubcarrierModulation()->getCodeWordSize() <= sourceModulation && + candidateMcs->getCode()->getForwardErrorCorrection()->getCodeRate() <= sourceCodeRate && + (response == nullptr || candidateDataMode->getMcsIndex() > response->getDataMode()->getMcsIndex())) + response = candidate; + } + if (response != nullptr) + result.emplace(sourceEntry.mode, response); + } + return result; +} + +std::map Ieee80211ModeSet::createHtMixedControlResponseModes(const std::vector& supportedEntries) +{ + std::map result; + for (const auto& sourceEntry : supportedEntries) { + auto source = dynamic_cast(sourceEntry.mode); + if (source == nullptr) + continue; + for (const auto& candidateEntry : supportedEntries) { + auto candidate = dynamic_cast(candidateEntry.mode); + if (candidate != nullptr && candidate->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + candidate->getCenterFrequencyMode() == source->getCenterFrequencyMode() && + candidate->getDataMode()->getMcsIndex() == source->getDataMode()->getMcsIndex() && + candidate->getDataMode()->getBandwidth() == source->getDataMode()->getBandwidth() && + candidate->getDataMode()->getNumberOfSpatialStreams() == source->getDataMode()->getNumberOfSpatialStreams() && + candidate->getDataMode()->getGuardIntervalType() == source->getDataMode()->getGuardIntervalType()) + { + result.emplace(sourceEntry.mode, candidateEntry.mode); + break; + } + } + } + return result; +} + +std::vector Ieee80211ModeSet::createNonHtControlResponseEntries(const std::vector& supportedEntries) +{ + std::vector result; + for (const auto& entry : supportedEntries) + if (entry.isMandatory && dynamic_cast(entry.mode) == nullptr && dynamic_cast(entry.mode) == nullptr) + result.push_back(entry); + std::stable_sort(result.begin(), result.end(), EntryNetBitrateComparator()); + return result; +} + +bool Ieee80211ModeSet::supportsMode(const IIeee80211Mode *mode) const +{ + for (const auto& entry : supportedEntries) + if (entry.mode == mode) + return true; + return false; +} + int Ieee80211ModeSet::getModeIndex(const IIeee80211Mode *mode) const { int index = findModeIndex(mode); @@ -681,7 +1000,32 @@ int Ieee80211ModeSet::getModeIndex(const IIeee80211Mode *mode) const bool Ieee80211ModeSet::getIsMandatory(const IIeee80211Mode *mode) const { - return entries[getModeIndex(mode)].isMandatory; + int index = findModeIndex(mode); + if (index >= 0) + return entries[index].isMandatory; + for (const auto *legacyMode : legacyOperationalModes) { + if (legacyMode == mode) { + for (const auto& entry : supportedEntries) { + if (entry.mode == mode) + return entry.isMandatory; + } + } + } + throw cRuntimeError("Unknown mode"); +} + +const IIeee80211Mode *Ieee80211ModeSet::findMode(const IIeee80211Mode *mode) const +{ + int index = findModeIndex(mode); + return index >= 0 ? entries[index].mode : nullptr; +} + +const IIeee80211Mode *Ieee80211ModeSet::getMode(const IIeee80211Mode *mode) const +{ + auto result = findMode(mode); + if (result == nullptr) + throw cRuntimeError("Unknown mode in operation mode: '%s'", getName()); + return result; } const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode *mode) const @@ -867,6 +1211,78 @@ const IIeee80211Mode *Ieee80211ModeSet::getFasterMandatoryMode(const IIeee80211M return result; } +const IIeee80211Mode *Ieee80211ModeSet::getControlResponseMode(const IIeee80211Mode *mode, const IIeee80211Mode *configuredMode) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + auto it = controlResponseModes.find(mode); + if (it == controlResponseModes.end()) { + if (configuredMode != nullptr) + return getNonHtControlResponseMode(configuredMode, false); + return getMandatoryControlResponseMode(mode); + } + auto primaryMode = it->second; + if (configuredMode == nullptr) + return primaryMode; + // A configured HT response is a deliberate model extension beyond the + // automatic response constraints in IEEE 802.11-2024 10.6.6.5.3 and + // 10.6.6.5.7. Translate only its preamble to HT-mixed, preserving the + // resolved configured mode's MCS, bandwidth, NSS, GI, and band. + if (!supportsMode(configuredMode)) + throw cRuntimeError("Configured control response mode is not supported by operation mode %s: %s", getName(), configuredMode->getName()); + auto configuredIt = htMixedControlResponseModes.find(configuredMode); + if (configuredIt == htMixedControlResponseModes.end()) + throw cRuntimeError("An HT RTS requires an HT-mixed CTS response, configured mode is non-HT: %s", configuredMode->getName()); + return configuredIt->second; +} + +const IIeee80211Mode *Ieee80211ModeSet::getMandatoryControlResponseMode(const IIeee80211Mode *mode) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + if (!nonHtControlResponseEntries.empty() && (controlResponseModes.find(mode) != controlResponseModes.end() || !containsMode(mode))) + return getNonHtControlResponseMode(mode); + if (getIsMandatory(mode)) + return mode; + if (auto slowerMode = getSlowerMandatoryMode(mode)) + return slowerMode; + return getNonHtControlResponseMode(mode); +} + +const IIeee80211Mode *Ieee80211ModeSet::getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + if (nonHtControlResponseEntries.empty()) { + if (!containsMode(mode)) + throw cRuntimeError("No non-HT control response mode for %s", mode->getName()); + if (!mandatory) + return mode; + if (getIsMandatory(mode)) + return mode; + if (auto slowerMode = getSlowerMandatoryMode(mode)) + return slowerMode; + throw cRuntimeError("No mandatory control response mode for %s", mode->getName()); + } + if (controlResponseModes.find(mode) == controlResponseModes.end()) { + // VHT response-format selection remains unchanged; this fallback is HT-scoped. + if (dynamic_cast(mode) != nullptr) + return mode; + if (!mandatory) + return mode; + } + const IIeee80211Mode *result = nullptr; + for (const auto& entry : nonHtControlResponseEntries) { + auto candidate = entry.mode; + if (candidate->getDataMode()->getNetBitrate() <= mode->getDataMode()->getNetBitrate() && + (result == nullptr || candidate->getDataMode()->getNetBitrate() > result->getDataMode()->getNetBitrate())) + result = candidate; + } + if (result == nullptr) + throw cRuntimeError("No mandatory non-HT control response mode for %s", mode->getName()); + return result; +} + const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) { for (size_t index = 0; index < (&modeSets)->size(); index++) { @@ -895,4 +1311,3 @@ const Ieee80211ModeSet *Ieee80211ModeSet::getModeSet(const char *mode) } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index b7b769105fe..23d96eaa08d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -28,7 +28,6 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject VHT, }; - protected: class INET_API Entry { public: bool isMandatory; @@ -55,17 +54,26 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject std::set htSupportedChannelWidths; std::set htShortGuardIntervalChannelWidths; bool htOperationSupported = false; + // Entries are selectable modes; supportedEntries also contains immutable PHY capabilities needed for mandatory control responses. + const std::vector supportedEntries; + const std::map controlResponseModes; + const std::map htMixedControlResponseModes; + const std::vector nonHtControlResponseEntries; public: - static const DelayedInitializer> modeSets; + static OPP_THREAD_LOCAL const DelayedInitializer> modeSets; protected: + static std::vector completeHtGuardIntervalVariants(const char *name, const std::vector& entries); int findModeIndex(const IIeee80211Mode *mode) const; int getModeIndex(const IIeee80211Mode *mode) const; + static std::map createControlResponseModes(const std::vector& supportedEntries); + static std::map createHtMixedControlResponseModes(const std::vector& supportedEntries); + static std::vector createNonHtControlResponseEntries(const std::vector& supportedEntries); public: Ieee80211ModeSet(const char *name, const std::vector entries, const IIeee80211Mode *referenceMode, - PhyType phyType, bool htOperationSupported = false); + PhyType phyType, bool htOperationSupported = false, const std::vector supportedEntries = {}); virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "Ieee80211ModeSet, name = " << name; } @@ -88,12 +96,21 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const std::set& getHtShortGuardIntervalChannelWidths() const { return htShortGuardIntervalChannelWidths; } bool isHtShortGuardIntervalSupported(Hz bandwidth) const { return htShortGuardIntervalChannelWidths.count(bandwidth) != 0; } + // containsMode() covers entries selectable as the persistent operating mode + // (for example through Ieee80211Transmitter::setMode()). supportsMode() + // additionally covers immutable PHY capabilities that may be selected per + // packet through Ieee80211ModeReq, such as legacy control responses in a + // 2.4 GHz HT profile. bool containsMode(const IIeee80211Mode *mode) const { return findModeIndex(mode) != -1; } + bool supportsMode(const IIeee80211Mode *mode) const; bool getIsMandatory(const IIeee80211Mode *mode) const; // Finds a mode with the same PHY tuple as mode. Unlike findMode(), this // treats an absent guard interval (negative value) as an exact value. const IIeee80211Mode *findCompatibleMode(const IIeee80211Mode *mode) const; + // Pointer lookup is intentionally strict. Use getControlResponseMode() for an explicitly requested response that needs HT-mixed translation. + const IIeee80211Mode *findMode(const IIeee80211Mode *mode) const; + const IIeee80211Mode *getMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; @@ -108,6 +125,18 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const IIeee80211Mode *getSlowerMandatoryMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getFasterMandatoryMode(const IIeee80211Mode *mode) const; + // Automatic responses follow IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7. + // A configured CTS mode must be selectable and, for an HT RTS, must be HT; + // it is translated only to the corresponding HT-mixed response. An explicitly + // configured HT mode is a deliberate override and may bypass those response + // constraints. A legacy configured CTS rate is rejected by rate-selection + // initialization, while this API keeps the direct HT/legacy combination fatal. + const IIeee80211Mode *getControlResponseMode(const IIeee80211Mode *mode, const IIeee80211Mode *configuredMode = nullptr) const; + const IIeee80211Mode *getMandatoryControlResponseMode(const IIeee80211Mode *mode) const; + // 2.4 GHz HT modes are converted to non-HT responses. With mandatory=false, + // the input bitrate is used as a ceiling for selecting that non-HT mode. + const IIeee80211Mode *getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory = true) const; + static const Ieee80211ModeSet *findModeSet(const char *mode); static const Ieee80211ModeSet *getModeSet(const char *mode); @@ -131,4 +160,3 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index bd553b35dae..055bc297c05 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -706,8 +706,8 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 throw cRuntimeError("Unsupported VHT preamble format: only HT_PREAMBLE_MIXED is supported (IEEE Std 802.11-2024, 21.3.2)"); const char *name = ""; // TODO unsigned int nss = mcsMode->getNumNss(); - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); - auto mode = singleton.modeCache.find(htModeId); + auto vhtModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); + auto mode = singleton.modeCache.find(vhtModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmSignalMode *legacySignal = nullptr; const Ieee80211VhtSignalMode *htSignal = nullptr; @@ -725,7 +725,7 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 const Ieee80211VhtDataMode *dataMode = new Ieee80211VhtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211VhtPreambleMode *preambleMode = new Ieee80211VhtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211VhtMode *htMode = new Ieee80211VhtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair(htModeId, htMode)); + singleton.modeCache.emplace(vhtModeId, htMode); return htMode; } return mode->second; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index ad9748e6094..5c0ff55e7e9 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -137,9 +137,10 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 abortIncompatibleReception(); if (modeSetCoordinator != nullptr) modeSetCoordinator->completeModeSetChange(modeSet); - else if (modeSet != nullptr) + else if (modeSet != nullptr && getComponentType() != nullptr) emit(modesetChangedSignal, const_cast(modeSet)); - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); changingModeSet = false; EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; } @@ -177,7 +178,8 @@ void Ieee80211Radio::setMode(const IIeee80211Mode *mode) Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); ieee80211Transmitter->setMode(mode); EV << "Changing radio mode to " << mode << endl; - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setBand(const IIeee80211Band *band) @@ -193,7 +195,8 @@ void Ieee80211Radio::setBand(const IIeee80211Band *band) Ieee80211RadioChannelChangedDetails details(channel->getBand()); emit(radioChannelChangedSignal, channel->getChannelNumber(), &details); } - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setChannel(const Ieee80211Channel *channel) @@ -208,7 +211,8 @@ void Ieee80211Radio::setChannel(const Ieee80211Channel *channel) abortIncompatibleReception(); Ieee80211RadioChannelChangedDetails details(channel->getBand()); emit(radioChannelChangedSignal, channel->getChannelNumber(), &details); - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setChannelNumber(int newChannelNumber) @@ -221,7 +225,8 @@ void Ieee80211Radio::setChannelNumber(int newChannelNumber) abortIncompatibleReception(); Ieee80211RadioChannelChangedDetails details(ieee80211Transmitter->getChannel()->getBand()); emit(radioChannelChangedSignal, newChannelNumber, &details); - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::insertFcs(const Ptr& phyHeader) const diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned index 56e5e2c1a9a..d4683437183 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned @@ -28,7 +28,7 @@ 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 opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)", "p", "ac") = default("g(mixed)"); // Operation mode string bandName @enum("2.4 GHz", "5 GHz", "5 GHz (20 MHz)", "5 GHz (40 MHz)", "5 GHz (80 MHz)", "5 GHz (160 MHz)", "5.9 GHz") = default("2.4 GHz"); // Band name int channelNumber = default(0); // Initial channel number within the band (TODO this is offset by 1) string fcsMode @enum("declared","computed") = default("declared"); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 7609deade75..9514cdccb28 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -50,13 +50,13 @@ std::ostream& Ieee80211Receiver::printToStream(std::ostream& stream, int level, bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, const ITransmission *transmission) const { auto ieee80211Transmission = dynamic_cast(transmission); - return ieee80211Transmission && modeSet->containsMode(ieee80211Transmission->getMode()) && NarrowbandReceiverBase::computeIsReceptionPossible(listening, transmission); + return ieee80211Transmission && modeSet->supportsMode(ieee80211Transmission->getMode()) && NarrowbandReceiverBase::computeIsReceptionPossible(listening, transmission); } bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, const IReception *reception, IRadioSignal::SignalPart part) const { auto ieee80211Transmission = dynamic_cast(reception->getTransmission()); - return ieee80211Transmission && modeSet->containsMode(ieee80211Transmission->getMode()) && getAnalogModel()->computeIsReceptionPossible(listening, reception, sensitivity); + return ieee80211Transmission && modeSet->supportsMode(ieee80211Transmission->getMode()) && getAnalogModel()->computeIsReceptionPossible(listening, reception, sensitivity); } const IReceptionResult *Ieee80211Receiver::computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned index 3d1a0bc720d..8f2e716167a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned @@ -25,7 +25,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandReceiverBas module Ieee80211Receiver extends NarrowbandReceiverBase like IIeee80211ReceiverCapabilities { parameters: - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); int channelNumber; modulation = default("BPSK"); // TODO this is simply wrong @@ -34,4 +34,3 @@ module Ieee80211Receiver extends NarrowbandReceiverBase like IIeee80211ReceiverC *.opMode = this.opMode; @class(Ieee80211Receiver); } - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index bcbe608b091..a0763a09c7b 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -49,7 +49,7 @@ const IIeee80211Mode *Ieee80211Transmitter::computeTransmissionMode(const Packet const auto& modeReq = const_cast(packet)->findTag(); const auto& bitrateReq = const_cast(packet)->findTag(); if (modeReq != nullptr) { - if (modeSet != nullptr && !modeSet->containsMode(modeReq->getMode())) + if (modeSet != nullptr && !modeSet->supportsMode(modeReq->getMode())) throw cRuntimeError("Unsupported mode requested"); transmissionMode = modeReq->getMode(); } @@ -174,9 +174,18 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); - const simtime_t headerDuration = transmissionMode->getHeaderDuration(); - const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); + const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); + const simtime_t modeledDataDuration = transmissionMode->getDataMode()->getDuration(B(phyHeader->getLengthField())); + // HT/VHT include their SIG fields in the PHY preamble duration, so their + // mode duration is exactly preamble + modeled data. Other PHYs expose a + // separate header; their residual data interval may also include a trailing + // signal extension (ERP). Keep that extension in chronological data time. + const bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); + const simtime_t dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; + if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || + preambleDuration + headerDuration + dataDuration != duration) + throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); auto analogModel = getAnalogModel()->createAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, transmissionBandwidth, transmissionPower); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, nullptr, nullptr, nullptr, nullptr, analogModel, transmissionMode, transmissionChannel); } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned index 7d5cedb7505..ddd1c96d80b 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned @@ -19,7 +19,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandTransmitter module Ieee80211Transmitter extends NarrowbandTransmitterBase like IIeee80211TransmitterCapabilities { parameters: - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); int channelNumber; modulation = default("BPSK"); // TODO: This is simply wrong diff --git a/tests/unit/Ieee80211HtGreenfield_1.test b/tests/unit/Ieee80211HtGreenfield_1.test new file mode 100644 index 00000000000..11a5efa886f --- /dev/null +++ b/tests/unit/Ieee80211HtGreenfield_1.test @@ -0,0 +1,354 @@ +%description: +Checks that the 802.11n mixed and Greenfield mode profiles preserve distinct +preamble formats and timing, retain strict membership, and support the legacy +2.4 GHz HT capabilities used for non-HT control responses. Also checks VHT cache identity. + +%includes: +#include +#include + +#include "inet/common/packet/Packet.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TestIeee80211Transmitter : public Ieee80211Transmitter +{ + public: + const Ieee80211ModeSet *getTestModeSet() const { return modeSet; } + const IIeee80211Mode *getTestMode() const { return mode; } + const IIeee80211Mode *getTestTransmissionMode(const Packet *packet) const { return computeTransmissionMode(packet); } +}; + +static const Ieee80211HtMode *findMcs0LongGiMode(const Ieee80211ModeSet *modeSet) +{ + auto mode = dynamic_cast(modeSet->getMode(Mbps(6.5), MHz(20), 1)); + return mode != nullptr && mode->getDataMode()->getModulationAndCodingScheme()->getMcsIndex() == 0 && + mode->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG ? mode : nullptr; +} + +static bool rejectsUnknownMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + try { + modeSet->getMode(mode); + return false; + } + catch (const cRuntimeError&) { + return true; + } +} + +static bool rejectsMandatoryLookup(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + try { + modeSet->getIsMandatory(mode); + return false; + } + catch (const cRuntimeError&) { + return true; + } +} + +static bool rejectsLegacyConfiguredCts(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *htMode, const IIeee80211Mode *legacyMode) +{ + try { + modeSet->getControlResponseMode(htMode, legacyMode); + return false; + } + catch (const cRuntimeError& error) { + return std::string(error.getFormattedMessage()).find("HT RTS requires an HT-mixed CTS response") != std::string::npos; + } +} + +static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode) +{ + auto duration = mode->getDuration(B(0)); + auto preambleDuration = mode->getPreambleMode()->getDuration(); + auto modeledDataDuration = mode->getDataMode()->getDuration(B(0)); + bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + auto headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : mode->getHeaderMode()->getDuration(); + auto dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; + ASSERT(preambleDuration >= SIMTIME_ZERO); + ASSERT(headerDuration >= SIMTIME_ZERO); + ASSERT(dataDuration >= SIMTIME_ZERO); + ASSERT(preambleDuration + headerDuration + dataDuration == duration); + if (dynamic_cast(mode) != nullptr) + ASSERT(headerDuration == SIMTIME_ZERO); + if (dynamic_cast(mode) != nullptr) + ASSERT(dataDuration > modeledDataDuration); + if (dynamic_cast(mode) != nullptr && dynamic_cast(mode) == nullptr) + ASSERT(headerDuration > SIMTIME_ZERO); +} + +%activity: +auto mixedProfile = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +auto greenfieldProfile = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"); +ASSERT(mixedProfile != nullptr); +ASSERT(greenfieldProfile != nullptr); +ASSERT(mixedProfile == Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); +ASSERT(greenfieldProfile == Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)")); + +auto mixedMode = findMcs0LongGiMode(mixedProfile); +auto greenfieldMode = findMcs0LongGiMode(greenfieldProfile); +ASSERT(mixedMode != nullptr); +ASSERT(greenfieldMode != nullptr); +ASSERT(mixedMode != greenfieldMode); +ASSERT(mixedMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_2_4GHZ); +ASSERT(greenfieldMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_2_4GHZ); +ASSERT(mixedMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); +ASSERT(greenfieldMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(mixedMode->getPreambleMode()->getDuration() == SimTime(36, SIMTIME_US)); +ASSERT(greenfieldMode->getPreambleMode()->getDuration() == SimTime(24, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) == SimTime(40, SIMTIME_US)); +ASSERT(greenfieldMode->getDuration(B(0)) == SimTime(28, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) - greenfieldMode->getDuration(B(0)) == SimTime(12, SIMTIME_US)); +ASSERT(mixedMode->getDataMode()->getDuration(B(0)) == SimTime(4, SIMTIME_US)); +ASSERT(greenfieldMode->getDataMode()->getDuration(B(0)) == SimTime(4, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) == mixedMode->getPreambleMode()->getDuration() + mixedMode->getDataMode()->getDuration(B(0))); +ASSERT(greenfieldMode->getDuration(B(0)) == greenfieldMode->getPreambleMode()->getDuration() + greenfieldMode->getDataMode()->getDuration(B(0))); +ASSERT(!mixedProfile->containsMode(greenfieldMode)); +ASSERT(!greenfieldProfile->containsMode(mixedMode)); +ASSERT(mixedProfile->getSlowerMode(greenfieldMode) == nullptr); +ASSERT(greenfieldProfile->getFasterMode(mixedMode) == nullptr); +ASSERT(rejectsMandatoryLookup(mixedProfile, greenfieldMode)); +ASSERT(rejectsMandatoryLookup(greenfieldProfile, mixedMode)); +ASSERT(rejectsUnknownMode(mixedProfile, greenfieldMode)); +ASSERT(rejectsUnknownMode(greenfieldProfile, mixedMode)); +ASSERT(mixedProfile->supportsMode(mixedMode)); +ASSERT(greenfieldProfile->supportsMode(greenfieldMode)); +ASSERT(greenfieldProfile->supportsMode(mixedMode)); +ASSERT(!mixedProfile->supportsMode(greenfieldMode)); +ASSERT(mixedProfile->getSifsTime() == SimTime(10, SIMTIME_US)); +ASSERT(greenfieldProfile->getSifsTime() == SimTime(10, SIMTIME_US)); +ASSERT(mixedProfile->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(greenfieldProfile->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(mixedProfile->getCwMin() == 15); +ASSERT(greenfieldProfile->getCwMin() == 15); +const IIeee80211Mode *mandatoryLegacyModes[] = { + &Ieee80211DsssCompliantModes::dsssMode1Mbps, + &Ieee80211DsssCompliantModes::dsssMode2Mbps, + &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, +}; +for (auto legacyMode : mandatoryLegacyModes) { + ASSERT(mixedProfile->supportsMode(legacyMode)); + ASSERT(greenfieldProfile->supportsMode(legacyMode)); +} +const IIeee80211Mode *supplementaryOnlyLegacyModes[] = { + &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble, +}; +for (auto legacyMode : supplementaryOnlyLegacyModes) { + ASSERT(!mixedProfile->containsMode(legacyMode)); + ASSERT(!greenfieldProfile->containsMode(legacyMode)); +} +ASSERT(Ieee80211DsssCompliantModes::dsssMode1Mbps.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211DsssCompliantModes::dsssMode2Mbps.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(mixedProfile->getNonHtControlResponseMode(mixedMode) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(greenfieldProfile->getNonHtControlResponseMode(greenfieldMode) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(greenfieldProfile->getNonHtControlResponseMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode9Mbps) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(mixedProfile->getControlResponseMode(mixedMode) == mixedMode); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMode) == mixedMode); + +auto greenfieldMcs8 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldProfile->supportsMode(greenfieldMcs8)); +// MCS 8 is optional and uses two BPSK 1/2 streams. With no Basic HT-MCS Set +// modelled, 10.6.6.5.3 selects mandatory one-stream MCS 0 for the CTS. +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8) == mixedMode); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8, greenfieldMode) == mixedMode); +ASSERT(rejectsLegacyConfiguredCts(greenfieldProfile, greenfieldMode, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps)); + +auto greenfieldMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +auto mixedMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +auto greenfieldMcs15 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs7) == mixedMcs7); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs15) == mixedMcs7); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs7, greenfieldMode) == mixedMode); + +auto mixedMcs15 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +auto configuredMixedResponse = dynamic_cast(greenfieldProfile->getControlResponseMode(greenfieldMcs7, greenfieldMcs15)); +ASSERT(configuredMixedResponse == mixedMcs15); +ASSERT(configuredMixedResponse->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); +ASSERT(configuredMixedResponse->getCenterFrequencyMode() == greenfieldMcs15->getCenterFrequencyMode()); +ASSERT(configuredMixedResponse->getDataMode()->getMcsIndex() == greenfieldMcs15->getDataMode()->getMcsIndex()); +ASSERT(configuredMixedResponse->getDataMode()->getBandwidth() == greenfieldMcs15->getDataMode()->getBandwidth()); +ASSERT(configuredMixedResponse->getDataMode()->getNumberOfSpatialStreams() == greenfieldMcs15->getDataMode()->getNumberOfSpatialStreams()); +ASSERT(configuredMixedResponse->getDataMode()->getGuardIntervalType() == greenfieldMcs15->getDataMode()->getGuardIntervalType()); +ASSERT(mixedProfile->getMandatoryControlResponseMode(mixedMcs7) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); +ASSERT(mixedProfile->getMandatoryControlResponseMode(mixedMcs15) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); +ASSERT(mixedProfile->getNonHtControlResponseMode(mixedMcs15, false) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); + +auto greenfieldMcs8Bw40 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW40MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +auto mixedMcs0Bw40 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs0BW40MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +// The bounded no-Basic-HT-MCS-Set approximation reuses mandatory 20 MHz MCS +// indexes for candidates at the source bandwidth, so optional 40 MHz MCS 8 +// maps to 40 MHz MCS 0 (without changing the established response behavior). +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8Bw40) == mixedMcs0Bw40); + +auto transmitter = new TestIeee80211Transmitter; +transmitter->setModeSet(greenfieldProfile); +transmitter->setMode(greenfieldMode); +transmitter->setModeSet(mixedProfile); +ASSERT(transmitter->getTestModeSet() == mixedProfile); +ASSERT(transmitter->getTestMode() == mixedMode); +transmitter->setModeSet(greenfieldProfile); +ASSERT(transmitter->getTestModeSet() == greenfieldProfile); +ASSERT(transmitter->getTestMode() == greenfieldMode); +Packet supportedLegacyPacket("supportedLegacy"); +supportedLegacyPacket.addTag()->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(transmitter->getTestTransmissionMode(&supportedLegacyPacket) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setModeSet(mixedProfile); +ASSERT(transmitter->getTestTransmissionMode(&supportedLegacyPacket) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(transmitter->getTestMode() == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setMode(mixedMode); +transmitter->setModeSet(greenfieldProfile); +try { + transmitter->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestMode() == greenfieldMode); +} +try { + transmitter->setMode(mixedMode); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestMode() == greenfieldMode); +} +transmitter->setModeSet(nullptr); +ASSERT(transmitter->getTestModeSet() == nullptr); +ASSERT(transmitter->getTestMode() == nullptr); +delete transmitter; + +auto ofdmProfile = Ieee80211ModeSet::getModeSet("a"); +auto halfRateProfile = Ieee80211ModeSet::getModeSet("p"); +auto ofdm6 = ofdmProfile->getMode(Mbps(6), MHz(20), 1); +transmitter = new TestIeee80211Transmitter; +transmitter->setModeSet(ofdmProfile); +transmitter->setMode(ofdm6); +try { + transmitter->setModeSet(halfRateProfile); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestModeSet() == ofdmProfile); + ASSERT(transmitter->getTestMode() == ofdm6); +} +delete transmitter; + +auto greenfield5GhzMode = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs0BW20MHz, + Ieee80211HtMode::BAND_5GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(greenfield5GhzMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_5GHZ); +ASSERT(greenfield5GhzMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(!greenfieldProfile->containsMode(greenfield5GhzMode)); +ASSERT(!greenfieldProfile->supportsMode(greenfield5GhzMode)); + +auto vhtMixed5GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +bool rejectedVhtGreenfield = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (const cRuntimeError&) { + rejectedVhtGreenfield = true; +} +ASSERT(rejectedVhtGreenfield); +auto vhtMixed2GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_2_4GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(vhtMixed5GhzMode != vhtMixed2GhzMode); +ASSERT(vhtMixed5GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_5GHZ); +ASSERT(vhtMixed2GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_2_4GHZ); +ASSERT(vhtMixed5GhzMode->getDuration(B(0)) == vhtMixed5GhzMode->getPreambleMode()->getDuration() + vhtMixed5GhzMode->getDataMode()->getDuration(B(0))); +ASSERT(vhtMixed5GhzMode->getDataMode()->getDuration(B(0)) >= SIMTIME_ZERO); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6))); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode); + +auto erpMixedProfile = Ieee80211ModeSet::getModeSet("g(mixed)"); +auto erpOnlyProfile = Ieee80211ModeSet::getModeSet("g(erp)"); +auto erpMixedMode = &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps; +auto erpOnlyMode = &Ieee80211ErpOfdmCompliantModes::erpOnlyOfdmMode6Mbps; +Ieee80211DsssOfdmMode dsssOfdmMode("dsss-ofdm", nullptr, nullptr, nullptr, nullptr, &Ieee80211OfdmCompliantModes::ofdmDataMode6MbpsCS20MHz); +ASSERT(erpMixedProfile != nullptr); +ASSERT(erpOnlyProfile != nullptr); +assertTransmitterDurationDecomposition(erpMixedProfile->getMode(Mbps(6))); +ASSERT(erpMixedMode != erpOnlyMode); +ASSERT(erpMixedMode->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(erpOnlyMode->getSlotTime() == SimTime(9, SIMTIME_US)); +ASSERT(!erpMixedProfile->containsMode(erpOnlyMode)); +ASSERT(!erpOnlyProfile->containsMode(erpMixedMode)); +ASSERT(erpMixedProfile->findMode(erpOnlyMode) == nullptr); +ASSERT(erpOnlyProfile->findMode(erpMixedMode) == nullptr); +ASSERT(erpMixedProfile->findMode(&dsssOfdmMode) == nullptr); +ASSERT(rejectsUnknownMode(erpMixedProfile, erpOnlyMode)); +ASSERT(rejectsUnknownMode(erpOnlyProfile, erpMixedMode)); + +EV << "HT mixed and Greenfield modes have distinct cached identities and timing.\n"; + +%contains: stdout +HT mixed and Greenfield modes have distinct cached identities and timing. diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 8c5cec83e78..a2fcd996b6f 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -53,10 +53,11 @@ class TestIeee80211Receiver : public Ieee80211Receiver class TestIeee80211Radio : public Ieee80211Radio { public: - void setup(ITransmitter *tx, IReceiver *rx) + void setup(Ieee80211Transmitter *tx, Ieee80211Receiver *rx) { transmitter = tx; receiver = rx; + setComponentType(cModuleType::get("inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio")); } }; From d7cdba11c63c0f6399d961c694dbd42a97e19638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 01:48:34 +0200 Subject: [PATCH 49/91] feature(linklayer): implement HT control-response rate selection and Greenfield integration Apply IEEE 802.11-2024 control-response rules in DCF and QoS rate selection: ordinary HT ACK and Basic BlockAck responses use mandatory non-HT rates, and CTS responses to HT-carried RTS frames use the HT-mixed format. Translate configured CTS response rates to their corresponding HT-mixed counterparts while preserving MCS, bandwidth, NSS, and guard interval. Enforce mode set initialization invariants by failing fast if RateSelection has no mode set at link-layer initialization, and rebuild configured modes atomically on dynamic mode-set changes via modesetChangedSignal. Add "n(greenfield-2.4Ghz)" to wireless interface and MAC module NEDs, and provide an end-to-end Greenfield ping simulation example in omnetpp-ht-greenfield.ini. Document backward compatibility notes in WHATSNEW regarding 802.11n control-response rate selection. Add comprehensive unit and module test coverage for rate selection, dynamic mode-set rebinding, and HT Greenfield and Mixed runtime exchanges. --- WHATSNEW | 7 + .../lan80211/omnetpp-ht-greenfield.ini | 37 ++++ .../ieee80211/ExtUpperIeee80211Interface.ned | 3 +- .../ieee80211/Ieee80211Interface.ned | 2 +- .../linklayer/ieee80211/mac/Ieee80211Mac.ned | 2 +- .../mac/rateselection/QosRateSelection.cc | 102 +++++---- .../mac/rateselection/QosRateSelection.h | 1 - .../mac/rateselection/QosRateSelection.ned | 10 + .../mac/rateselection/RateSelection.cc | 71 ++++-- .../mac/rateselection/RateSelection.h | 1 - .../mac/rateselection/RateSelection.ned | 7 + ...e80211ConfiguredResponseRateSelection.test | 206 ++++++++++++++++++ .../module/Ieee80211HtGreenfieldRuntime.test | 132 +++++++++++ tests/module/Ieee80211HtMixedRuntime.test | 131 +++++++++++ .../Ieee80211InvalidConfiguredCtsRate.test | 134 ++++++++++++ .../Ieee80211LegacyResponseRateSelection.test | 110 ++++++++++ ...ateSelectionNullModeSetInitialization.test | 54 +++++ ...ateSelectionNullModeSetInitialization.test | 54 +++++ tests/unit/Ieee80211HtGuardInterval_1.test | 41 ++-- 19 files changed, 1025 insertions(+), 80 deletions(-) create mode 100644 examples/wireless/lan80211/omnetpp-ht-greenfield.ini create mode 100644 tests/module/Ieee80211ConfiguredResponseRateSelection.test create mode 100644 tests/module/Ieee80211HtGreenfieldRuntime.test create mode 100644 tests/module/Ieee80211HtMixedRuntime.test create mode 100644 tests/module/Ieee80211InvalidConfiguredCtsRate.test create mode 100644 tests/module/Ieee80211LegacyResponseRateSelection.test create mode 100644 tests/module/QosRateSelectionNullModeSetInitialization.test create mode 100644 tests/module/RateSelectionNullModeSetInitialization.test diff --git a/WHATSNEW b/WHATSNEW index 11a6544efaa..304f7700242 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -531,6 +531,13 @@ Notable backward incompatible changes are the following: These changes are backward incompatible for C++ code that directly references the old combined ICMP error indication or tag types. +7. 802.11n control-response rate selection + + Ordinary ACK and Basic BlockAck responses in the n(mixed-2.4Ghz) profile now + use the mandatory non-HT fallback because BSSBasicRateSet is not modelled. + This can change airtime, timeout trajectories, throughput, and fingerprints + in existing simulations. An HT RTS continues to receive an HT-mixed CTS. + Notable backward compatible changes are the following: 1. IPv6 network configurator diff --git a/examples/wireless/lan80211/omnetpp-ht-greenfield.ini b/examples/wireless/lan80211/omnetpp-ht-greenfield.ini new file mode 100644 index 00000000000..9d59b48fcc6 --- /dev/null +++ b/examples/wireless/lan80211/omnetpp-ht-greenfield.ini @@ -0,0 +1,37 @@ +[General] +network = Lan80211 +abstract = true +cmdenv-express-mode = true + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 600m +**.constraintAreaMaxY = 400m +**.constraintAreaMaxZ = 0m + +**.arp.typename = "GlobalArp" +**.opMode = "n(greenfield-2.4Ghz)" +**.wlan[*].bitrate = 13Mbps + +*.ap.wlan[*].address = "10:00:00:00:00:00" +*.host[*].**.mgmt.accessPointAddress = "10:00:00:00:00:00" + +*.ap.mobility.typename = "StationaryMobility" +*.ap.mobility.initialX = 250m +*.ap.mobility.initialY = 200m +*.host[*].mobility.typename = "StationaryMobility" +*.host[*].mobility.initialY = 200m +*.host[0].mobility.initialX = 260m +*.host[1].mobility.initialX = 270m + +[Config HtGreenfield] +description = "HT Greenfield mode exchanging ping traffic through an access point" +sim-time-limit = 2s +*.numHosts = 2 +*.host[0].numApps = 0 +*.host[1].numApps = 1 +*.host[1].app[0].typename = "PingApp" +*.host[1].app[0].destAddr = "host[0]" +*.host[1].app[0].sendInterval = 100ms +*.host[1].app[0].printPing = true diff --git a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned index 83b958c0f9f..57161c6354f 100644 --- a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned +++ b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned @@ -27,7 +27,7 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface { parameters: string energySourceModule = default(""); - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); double bitrate @unit(bps) = default(-1bps); **.opMode = this.opMode; **.bitrate = this.bitrate; @@ -92,4 +92,3 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface classifier.in <-- { @display("m=n"); } <-- tap.lowerLayerOut; } - diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned index 97b87db8034..bb8e516ab86 100644 --- a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned @@ -63,7 +63,7 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface parameters: string interfaceTableModule; string energySourceModule = default(""); - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); string address @mutable = default("auto"); // MAC address as hex string (12 hex digits), or // "auto". "auto" values will be replaced by // a generated MAC address in init stage 0. diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned index 09c308a0695..319581a53f8 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned @@ -80,7 +80,7 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac, IIeee80211MacCon { parameters: string mibModule; - string modeSet @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string modeSet @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); string fcsMode @enum("declared","computed") = default("declared"); string initialRadioMode @enum("off","sleep","receiver","transmitter","transceiver") = default("receiver"); diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index ab2226e6838..aa6bd4b9a3a 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -18,6 +18,23 @@ namespace ieee80211 { using namespace inet::physicallayer; +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +{ + if (bitrate == -1) + return nullptr; + try { + auto result = modeSet->getMode(bps(bitrate)); + if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) + throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); + return result; + } + catch (const cRuntimeError& error) { + throw cRuntimeError("%s has invalid responseCtsFrameBitrate=%g bps for operation mode '%s'; " + "the configured CTS rate must resolve to a selectable mode (HT RTS responses require an HT mode): %s", + modulePath, bitrate, modeSet->getName(), error.getFormattedMessage().c_str()); + } +} + Define_Module(QosRateSelection); void QosRateSelection::initialize(int stage) @@ -26,8 +43,9 @@ void QosRateSelection::initialize(int stage) if (stage == INITSTAGE_LOCAL) mib.reference(this, "mibModule", true); if (stage == INITSTAGE_LINK_LAYER) { - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); + if (modeSet == nullptr) + throw cRuntimeError("QosRateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); updateModes(); } } @@ -57,22 +75,30 @@ void QosRateSelection::updateModes() { if (modeSet == nullptr) return; - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - responseBlockAckFrameMode = (responseBlockAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); + + multicastFrameMode = newMulticastFrameMode; + dataFrameMode = newDataFrameMode; + mgmtFrameMode = newMgmtFrameMode; + controlFrameMode = newControlFrameMode; + responseAckFrameMode = newResponseAckFrameMode; + responseBlockAckFrameMode = newResponseBlockAckFrameMode; + responseCtsFrameMode = newResponseCtsFrameMode; + fastestMandatoryMode = newFastestMandatoryMode; lastTransmittedFrameMode.clear(); perReceiverDataFrameMode.clear(); perReceiverResolved = false; @@ -109,36 +135,27 @@ bool QosRateSelection::isControlResponseFrame(const Ptr& dataOrMgmtHeader) { - // TODO BSSBasicRateSet, alternate rate - auto mode = getMode(packet, dataOrMgmtHeader); - ASSERT(modeSet->containsMode(mode)); - const IIeee80211Mode *responseMode; - if (!responseAckFrameMode) { - if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) - responseMode = mandatoryMode; - else - throw cRuntimeError("Mandatory mode not found"); + const IIeee80211Mode *responseMode = nullptr; + if (responseAckFrameMode) + responseMode = modeSet->getNonHtControlResponseMode(responseAckFrameMode, false); + else { + auto mode = getMode(packet, dataOrMgmtHeader); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1/10.6.6.5.2: this bounded model uses a + // mandatory non-HT rate for ordinary ACK responses; BSSBasicRateSet is not modelled. + responseMode = modeSet->getMandatoryControlResponseMode(mode); } - else - responseMode = responseAckFrameMode; - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); + return dataOrMgmtHeader ? getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame) { - // TODO BSSBasicRateSet, alternate rate auto mode = getMode(packet, rtsFrame); - ASSERT(modeSet->containsMode(mode)); - const IIeee80211Mode *responseMode; - if (!responseCtsFrameMode) { - if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) - responseMode = mandatoryMode; - else - throw cRuntimeError("Mandatory mode not found"); - } - else - responseMode = responseCtsFrameMode; - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); + ASSERT(modeSet->supportsMode(mode)); + // The eliciting mode is required even when a CTS rate is configured because + // the response format and CandidateMCSSet depend on the received PPDU. + auto responseMode = modeSet->getControlResponseMode(mode, responseCtsFrameMode); + return rtsFrame ? getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode) : responseMode; } // @@ -149,12 +166,19 @@ const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *pack // const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq) { - if (dynamicPtrCast(blockAckReq)) { - auto mode = responseBlockAckFrameMode ? responseBlockAckFrameMode : getMode(packet, blockAckReq); - return getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), mode); - } - else + if (!dynamicPtrCast(blockAckReq)) throw cRuntimeError("Unknown BlockAckReq frame type"); + const IIeee80211Mode *responseMode = nullptr; + if (responseBlockAckFrameMode) + responseMode = modeSet->getNonHtControlResponseMode(responseBlockAckFrameMode, false); + else { + auto mode = getMode(packet, blockAckReq); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.5.2 permits non-HT Basic BlockAck responses; + // this model has no BSSBasicRateSet/HT Control context to select another PPDU. + responseMode = modeSet->getNonHtControlResponseMode(mode, false); + } + return blockAckReq ? getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 60b353138f0..2d315f36b65 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -61,7 +61,6 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetModule 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 // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 35f9aa93181..9e0ba20b9e4 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -28,8 +28,18 @@ simple QosRateSelection extends SimpleModule double multicastFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT BlockAck PPDU. double responseBlockAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. A configured value must resolve to a + // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed + // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a + // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 65c70a74557..b12c3346a6c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -23,6 +23,23 @@ namespace ieee80211 { using namespace inet::physicallayer; +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +{ + if (bitrate == -1) + return nullptr; + try { + auto result = modeSet->getMode(bps(bitrate)); + if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) + throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); + return result; + } + catch (const cRuntimeError& error) { + throw cRuntimeError("%s has invalid responseCtsFrameBitrate=%g bps for operation mode '%s'; " + "the configured CTS rate must resolve to a selectable mode (HT RTS responses require an HT mode): %s", + modulePath, bitrate, modeSet->getName(), error.getFormattedMessage().c_str()); + } +} + Define_Module(RateSelection); void RateSelection::initialize(int stage) @@ -33,6 +50,8 @@ void RateSelection::initialize(int stage) } else if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); + if (modeSet == nullptr) + throw cRuntimeError("RateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); updateModes(); // WATCH(dataOrMgmtRateControl); @@ -78,18 +97,28 @@ void RateSelection::updateModes() if (modeSet == nullptr) return; double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); + + // Commit only after every configured mode has been resolved, so a failed + // runtime operation-mode change leaves the previous state intact. + multicastFrameMode = newMulticastFrameMode; + dataFrameMode = newDataFrameMode; + mgmtFrameMode = newMgmtFrameMode; + controlFrameMode = newControlFrameMode; + responseAckFrameMode = newResponseAckFrameMode; + responseCtsFrameMode = newResponseCtsFrameMode; + fastestMandatoryMode = newFastestMandatoryMode; lastTransmittedFrameMode.clear(); perReceiverDataFrameMode.clear(); perReceiverResolved = false; @@ -114,26 +143,30 @@ const IIeee80211Mode *RateSelection::getMode(Packet *packet, const Ptr& dataOrMgmtHeader) { + // Keep configured responses independent of the eliciting packet; callers may + // resolve a configured response while constructing a test packet. + const IIeee80211Mode *responseMode = nullptr; if (responseAckFrameMode) - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseAckFrameMode); + responseMode = modeSet->getNonHtControlResponseMode(responseAckFrameMode, false); else { auto mode = getMode(packet, dataOrMgmtHeader); - ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1/10.6.6.5.2: this bounded model uses a + // mandatory non-HT rate for ordinary ACK responses; BSSBasicRateSet is not modelled. + responseMode = modeSet->getMandatoryControlResponseMode(mode); } + return dataOrMgmtHeader ? getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame) { - if (responseCtsFrameMode) - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseCtsFrameMode); - else { - auto mode = getMode(packet, rtsFrame); - ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); - } + auto mode = getMode(packet, rtsFrame); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1 and 10.6.6.5.7 require an HT response to an + // HT RTS and forbid HT-GF for the response. Consequently, even a configured + // CTS rate needs the eliciting PPDU's mode tag to select a legal format/MCS. + auto responseMode = modeSet->getControlResponseMode(mode, responseCtsFrameMode); + return rtsFrame ? getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode) : responseMode; } // 802.11-1999 Std. diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index a1ea7a92cf6..720ff8a76fb 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -63,7 +63,6 @@ class INET_API RateSelection : public IRateSelection, public ModeSetModuleBase 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 // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 31f8effbe5e..ade0ff83e8f 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -24,7 +24,14 @@ simple RateSelection extends SimpleModule like IRateSelection double multicastFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. A configured value must resolve to a + // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed + // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a + // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest diff --git a/tests/module/Ieee80211ConfiguredResponseRateSelection.test b/tests/module/Ieee80211ConfiguredResponseRateSelection.test new file mode 100644 index 00000000000..e8212250c11 --- /dev/null +++ b/tests/module/Ieee80211ConfiguredResponseRateSelection.test @@ -0,0 +1,206 @@ +%description: +Checks through the public rate-selection API that 2.4 GHz HT ACK/BlockAck responses use +non-HT rates and configured HT CTS rates use exact HT-mixed counterparts. + +%file: Test.cc +#include +#include + +#include "inet/common/InitStages.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211ConfiguredResponseRateSelection { + +static void assertHtMixedResponse(const char *name, const IIeee80211Mode *mode) +{ + auto htMode = dynamic_cast(mode); + if (htMode == nullptr || htMode->getPreambleMode()->getPreambleFormat() != Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED || htMode->getDataMode()->getNetBitrate() != Mbps(6.5)) + throw cRuntimeError("%s did not return HT-mixed 6.5 Mbps (actual mode: %s, bitrate: %g)", name, mode->getName(), mode->getDataMode()->getNetBitrate().get()); +} + +static void assertNonHtResponse(const char *name, const IIeee80211Mode *mode) +{ + if (dynamic_cast(mode) != nullptr || mode->getDataMode()->getNetBitrate() != Mbps(6)) + throw cRuntimeError("%s did not return non-HT 6 Mbps", name); +} + +class ConfiguredResponseProbe : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + + auto dcfRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0].mac.dcf.rateSelection")); + auto qosRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0].mac.hcf.rateSelection")); + auto dynamicQosRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0].mac.hcf.rateSelection")); + auto dynamicWlan = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0]")); + auto dynamicMac = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0].mac")); + if (strcmp(dynamicWlan->par("opMode"), "n(mixed-2.4Ghz)") != 0 || strcmp(dynamicMac->par("modeSet"), "n(mixed-2.4Ghz)") != 0) + throw cRuntimeError("Dynamic response probe is not using the mixed profile (wlan: %s, mac: %s)", dynamicWlan->par("opMode").stringValue(), dynamicMac->par("modeSet").stringValue()); + + if (double(dcfRateSelection->par("responseAckFrameBitrate")) != 6.5e6 + || double(qosRateSelection->par("responseAckFrameBitrate")) != 6.5e6) + throw cRuntimeError("Configured response bitrate was not applied to both rate selections"); + + auto sourceMode = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)")->getMode(Mbps(6.5)); + auto mixedSourceMode = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")->getMode(Mbps(6.5)); + Packet packet("receivedFrame"); + packet.addTag()->setMode(sourceMode); + Packet mixedPacket("receivedMixedFrame"); + mixedPacket.addTag()->setMode(mixedSourceMode); + Packet nonHtPacket("receivedNonHtFrame"); + nonHtPacket.addTag()->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); + auto dataHeader = makeShared(); + dataHeader->setReceiverAddress(MacAddress("10:00:00:00:00:00")); + Packet dataPacket("configuredDataFrame"); + + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == sourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == sourceMode); + + assertNonHtResponse("DCF ACK", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + Packet untaggedPacket("untaggedRts"); + try { + dcfRateSelection->computeResponseCtsFrameMode(&untaggedPacket, nullptr); + throw cRuntimeError("Configured DCF CTS unexpectedly accepted an untagged eliciting packet"); + } + catch (const cRuntimeError& error) { + if (error.getFormattedMessage().find("Missing mode") == std::string::npos) + throw; + } + try { + qosRateSelection->computeResponseCtsFrameMode(&untaggedPacket, nullptr); + throw cRuntimeError("Configured QoS CTS unexpectedly accepted an untagged eliciting packet"); + } + catch (const cRuntimeError& error) { + if (error.getFormattedMessage().find("Missing mode") == std::string::npos) + throw; + } + assertHtMixedResponse("DCF CTS HT", dcfRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("DCF CTS non-HT", dcfRateSelection->computeResponseCtsFrameMode(&nonHtPacket, nullptr)); + assertNonHtResponse("QoS ACK", qosRateSelection->computeResponseAckFrameMode(&packet, nullptr)); + assertHtMixedResponse("QoS CTS HT", qosRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("QoS CTS non-HT", qosRateSelection->computeResponseCtsFrameMode(&nonHtPacket, nullptr)); + + auto basicBlockAckReq = makeShared(); + assertNonHtResponse("QoS Basic BlockAck", qosRateSelection->computeResponseBlockAckFrameMode(&packet, basicBlockAckReq)); + assertNonHtResponse("Dynamic mixed QoS ACK", dynamicQosRateSelection->computeResponseAckFrameMode(&mixedPacket, nullptr)); + assertHtMixedResponse("Dynamic mixed QoS CTS", dynamicQosRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertNonHtResponse("Dynamic mixed QoS Basic BlockAck", dynamicQosRateSelection->computeResponseBlockAckFrameMode(&mixedPacket, basicBlockAckReq)); + auto optionalHtMode = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); + Packet optionalHtPacket("receivedOptionalHtFrame"); + optionalHtPacket.addTag()->setMode(optionalHtMode); + assertHtMixedResponse("Configured CTS optional HT MCS", qosRateSelection->computeResponseCtsFrameMode(&optionalHtPacket, nullptr)); + auto greenfieldMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); + Packet greenfieldMcs7Packet("receivedGreenfieldMcs7Frame"); + greenfieldMcs7Packet.addTag()->setMode(greenfieldMcs7); + assertHtMixedResponse("Configured DCF CTS MCS 0 override for MCS 7", dcfRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); + assertHtMixedResponse("Configured CTS MCS 0 override for MCS 7", qosRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); + + // Exercise the actual signal path with a compatible mode-set transition. + // The configured data cache must bind to the exact profile object while + // the configured response operations remain usable after each refresh. + auto wlan = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0]")); + wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"))); + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == mixedSourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == mixedSourceMode); + assertHtMixedResponse("Rebound DCF CTS mixed", dcfRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertHtMixedResponse("Rebound QoS CTS mixed", qosRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertNonHtResponse("Rebound DCF ACK mixed", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS ACK mixed", qosRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS BlockAck mixed", qosRateSelection->computeResponseBlockAckFrameMode(nullptr, basicBlockAckReq)); + wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"))); + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == sourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == sourceMode); + assertHtMixedResponse("Rebound DCF CTS Greenfield", dcfRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertHtMixedResponse("Rebound QoS CTS Greenfield", qosRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("Rebound DCF ACK Greenfield", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS ACK Greenfield", qosRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS BlockAck Greenfield", qosRateSelection->computeResponseBlockAckFrameMode(nullptr, basicBlockAckReq)); + + std::cout << "Mixed and Greenfield ACK/Basic BlockAck use non-HT 6 Mbps; configured HT CTS uses the exact HT-mixed MCS override and requires an eliciting mode tag.\n"; + } +}; + +Define_Module(ConfiguredResponseProbe); + +} // namespace Ieee80211ConfiguredResponseRateSelection + +%file: test.ned + +import inet.examples.wireless.lan80211.Lan80211; + +simple ConfiguredResponseProbe +{ + @class(ConfiguredResponseProbe); +} + +network TestConfiguredResponses extends Lan80211 +{ + parameters: + numHosts = default(2); + submodules: + configuredResponseProbe: ConfiguredResponseProbe; +} + +%inifile: omnetpp.ini + +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestConfiguredResponses +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +[Config ConfiguredResponses] +abstract = false +network = TestConfiguredResponses +sim-time-limit = 1s +*.host[1].wlan[*].opMode = "n(mixed-2.4Ghz)" +**.opMode = "n(greenfield-2.4Ghz)" +**.qosStation = true +*.host[1].wlan[0].mac.hcf.rateSelection.responseAckFrameBitrate = -1bps +*.host[1].wlan[0].mac.hcf.rateSelection.responseCtsFrameBitrate = -1bps +*.host[1].wlan[0].mac.hcf.rateSelection.responseBlockAckFrameBitrate = -1bps +**.wlan[*].mac.dcf.rateSelection.responseAckFrameBitrate = 6.5Mbps +**.wlan[*].mac.dcf.rateSelection.responseCtsFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseAckFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseCtsFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseBlockAckFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameBandwidth = 20MHz +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameNumSpatialStreams = 1 +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 + +%extraargs: -c ConfiguredResponses + +%contains: stdout +Mixed and Greenfield ACK/Basic BlockAck use non-HT 6 Mbps; configured HT CTS uses the exact HT-mixed MCS override and requires an eliciting mode tag. diff --git a/tests/module/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test new file mode 100644 index 00000000000..60d013e74f3 --- /dev/null +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -0,0 +1,132 @@ +%description: + +Checks forced HT-Greenfield RTS/HT-mixed CTS plus unicast HT-Greenfield data and non-HT ACK responses between two ad hoc stations. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtGreenfieldRuntime { + +static OPP_THREAD_LOCAL int htGreenfieldRtsCount = 0; +static OPP_THREAD_LOCAL int htMixedCtsCount = 0; +static OPP_THREAD_LOCAL int htGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int nonHtAckCount = 0; + +class HtPreambleCheckingRadio : public Ieee80211Radio, public cListener +{ + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + auto isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; + auto isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + if (dynamicPtrCast(macHeader) && isHtGreenfield) + htGreenfieldRtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedCtsCount++; + if (dynamicPtrCast(macHeader) && isHtGreenfield) + htGreenfieldDataCount++; + if (dynamicPtrCast(macHeader) && mode == nullptr) + nonHtAckCount++; + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (htGreenfieldRtsCount == 0 || htMixedCtsCount == 0 || htGreenfieldDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected HT-GF RTS, HT-MF CTS, HT-GF data, and non-HT ACK transmissions"); + std::cout << "Observed " << htGreenfieldRtsCount << " HT-Greenfield RTS, " << htMixedCtsCount << " HT-mixed CTS, " << htGreenfieldDataCount << " HT-Greenfield data, and " << nonHtAckCount << " non-HT ACK response(s).\n"; + } +}; + +Define_Module(HtPreambleCheckingRadio); + +} // namespace Ieee80211HtGreenfieldRuntime + +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; + +module HtPreambleCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtPreambleCheckingRadio); +} + +network TestHtGreenfield +{ + parameters: + int numHosts = default(2); + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[numHosts]: AdhocHost; +} + +%inifile: omnetpp.ini + +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestHtGreenfield +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +**.wlan[*].radio.typename = "HtPreambleCheckingRadio" + +[Config HtPreambleRuntime] +abstract = false +network = TestHtGreenfield +sim-time-limit = 2s +*.numHosts = 2 +*.host[0].numApps = 1 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[1].numApps = 1 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 +*.host[0].app[0].sendInterval = 100ms +**.wlan[*].opMode = "n(greenfield-2.4Ghz)" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B + +%extraargs: -c HtPreambleRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* HT-Greenfield RTS, [1-9][0-9]* HT-mixed CTS, [1-9][0-9]* HT-Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test new file mode 100644 index 00000000000..cd81007886e --- /dev/null +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -0,0 +1,131 @@ +%description: +Checks a deterministic n(mixed-2.4Ghz) exchange with HT-mixed RTS/CTS and data +followed by an ERP-OFDM ACK. Basic BlockAck response selection is covered by +the focused public QoS API test because enabling a bounded BAR exchange would +require additional association and agreement setup. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtMixedRuntime { + +static OPP_THREAD_LOCAL int htMixedRtsCount = 0; +static OPP_THREAD_LOCAL int htMixedCtsCount = 0; +static OPP_THREAD_LOCAL int htMixedDataCount = 0; +static OPP_THREAD_LOCAL int erpOfdmAckCount = 0; + +class HtMixedCheckingRadio : public Ieee80211Radio, public cListener +{ + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *obj, cObject *) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedRtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedCtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedDataCount++; + if (dynamicPtrCast(macHeader) && dynamic_cast(transmission->getMode()) != nullptr) { + if (transmission->getMode()->getDataMode()->getBandwidth() != MHz(20)) + throw cRuntimeError("ERP-OFDM ACK did not use the 20 MHz channel bandwidth"); + erpOfdmAckCount++; + } + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (htMixedRtsCount == 0 || htMixedCtsCount == 0 || htMixedDataCount == 0 || erpOfdmAckCount == 0) + throw cRuntimeError("Expected HT-mixed RTS/CTS and data with ERP-OFDM ACK response"); + std::cout << "Observed " << htMixedRtsCount << " HT-mixed RTS, " << htMixedCtsCount << " HT-mixed CTS, " << htMixedDataCount << " HT-mixed data, and " << erpOfdmAckCount << " ERP-OFDM ACK response(s).\n"; + } +}; + +Define_Module(HtMixedCheckingRadio); + +} // namespace Ieee80211HtMixedRuntime + +%file: test.ned +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module HtMixedCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtMixedCheckingRadio); +} + +network TestHtMixedRuntime +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[2]: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestHtMixedRuntime +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 +**.wlan[*].radio.typename = "HtMixedCheckingRadio" + +[Config HtMixedRuntime] +abstract = false +network = TestHtMixedRuntime +sim-time-limit = 2s +*.host[0].numApps = 1 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[0].app[0].sendInterval = 100ms +*.host[1].numApps = 1 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B + +%extraargs: -c HtMixedRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* HT-mixed RTS, [1-9][0-9]* HT-mixed CTS, [1-9][0-9]* HT-mixed data, and [1-9][0-9]* ERP-OFDM ACK response\(s\). diff --git a/tests/module/Ieee80211InvalidConfiguredCtsRate.test b/tests/module/Ieee80211InvalidConfiguredCtsRate.test new file mode 100644 index 00000000000..294f3939aa0 --- /dev/null +++ b/tests/module/Ieee80211InvalidConfiguredCtsRate.test @@ -0,0 +1,134 @@ +%description: +Checks that a non-selectable legacy responseCtsFrameBitrate is rejected during +rate-selection resolution for both DCF and QoS selections and both 2.4 GHz HT profiles. + +%file: Test.cc +#include +#include +#include + +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211InvalidConfiguredCtsRate { + +template +static void checkRejected(const char *selectorName, Selector& selector) +{ + double bitrate = selector.par("responseCtsFrameBitrate"); + char formattedBitrate[64]; + std::snprintf(formattedBitrate, sizeof(formattedBitrate), "%g", bitrate); + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)"}) { + bool rejected = false; + try { + selector.resolveConfiguredModesForTest(Ieee80211ModeSet::getModeSet(profile)); + } + catch (const cRuntimeError& error) { + rejected = true; + auto message = error.getFormattedMessage(); + if (message.find(std::string("responseCtsFrameBitrate=") + formattedBitrate) == std::string::npos || + message.find("operation mode '" + std::string(profile) + "'") == std::string::npos || + message.find("selectable mode") == std::string::npos) + throw cRuntimeError("%s diagnostic for %s at %g bps is incomplete: %s", selectorName, profile, bitrate, message.c_str()); + } + if (!rejected) + throw cRuntimeError("%s accepted legacy CTS bitrate %g bps in %s", selectorName, bitrate, profile); + } +} + +class TestRateSelection : public RateSelection +{ + public: + void resolveConfiguredModesForTest(const Ieee80211ModeSet *modeSet) { applyModeSet(modeSet); } + + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) { + checkRejected("DCF RateSelection", *this); + std::cout << "DCF rejected configured legacy responseCtsFrameBitrate for both HT profiles.\n"; + } + } +}; + +class TestQosRateSelection : public QosRateSelection +{ + public: + void resolveConfiguredModesForTest(const Ieee80211ModeSet *modeSet) { applyModeSet(modeSet); } + + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) { + checkRejected("QoS RateSelection", *this); + std::cout << "QoS rejected configured legacy responseCtsFrameBitrate for both HT profiles.\n"; + } + } +}; + +Define_Module(TestRateSelection); +Define_Module(TestQosRateSelection); + +} // namespace Ieee80211InvalidConfiguredCtsRate + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; + +simple TestRateSelection extends RateSelection +{ + parameters: + @class(TestRateSelection); + rateControlModule = ""; + mibModule = ""; + responseCtsFrameBitrate = default(6Mbps); +} + +simple TestQosRateSelection extends QosRateSelection +{ + parameters: + @class(TestQosRateSelection); + rateControlModule = ""; + mibModule = ""; + responseCtsFrameBitrate = default(6Mbps); +} + +simple TestRateSelection24 extends TestRateSelection +{ + parameters: + responseCtsFrameBitrate = default(24Mbps); +} + +simple TestQosRateSelection24 extends TestQosRateSelection +{ + parameters: + responseCtsFrameBitrate = default(24Mbps); +} + +network TestInvalidConfiguredCtsRate +{ + submodules: + dcfRateSelection6: TestRateSelection; + dcfRateSelection24: TestRateSelection24; + qosRateSelection6: TestQosRateSelection; + qosRateSelection24: TestQosRateSelection24; +} + +%inifile: omnetpp.ini +[General] +network = TestInvalidConfiguredCtsRate +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 + +%contains: stdout +DCF rejected configured legacy responseCtsFrameBitrate for both HT profiles. +%contains: stdout +QoS rejected configured legacy responseCtsFrameBitrate for both HT profiles. diff --git a/tests/module/Ieee80211LegacyResponseRateSelection.test b/tests/module/Ieee80211LegacyResponseRateSelection.test new file mode 100644 index 00000000000..5fc607b867b --- /dev/null +++ b/tests/module/Ieee80211LegacyResponseRateSelection.test @@ -0,0 +1,110 @@ +%description: +Checks the public DCF and QoS response-rate APIs for representative legacy +operation modes, preserving mandatory ACK/CTS and Basic BlockAck selections. + +%file: Test.cc +#include + +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211LegacyResponseRateSelection { + +class TestRateSelection : public RateSelection +{ + public: + void setModeSetForTest(const Ieee80211ModeSet *newModeSet) { modeSet = newModeSet; } +}; + +class TestQosRateSelection : public QosRateSelection +{ + public: + void setModeSetForTest(const Ieee80211ModeSet *newModeSet) { modeSet = const_cast(newModeSet); } +}; + +struct LegacyProfileCase { + const char *name; + bps sourceBitrate; + bps expectedResponseBitrate; + bps expectedBlockAckBitrate; +}; + +static void checkCase(const LegacyProfileCase& testCase) +{ + auto modeSet = Ieee80211ModeSet::getModeSet(testCase.name); + auto sourceMode = modeSet->getMode(testCase.sourceBitrate); + auto expectedMode = modeSet->getMode(testCase.expectedResponseBitrate); + auto expectedBlockAckMode = modeSet->getMode(testCase.expectedBlockAckBitrate); + Packet packet("elicitingFrame"); + packet.addTag()->setMode(sourceMode); + auto basicBlockAckReq = makeShared(); + + TestRateSelection dcfRateSelection; + dcfRateSelection.setModeSetForTest(modeSet); + ASSERT(dcfRateSelection.computeResponseAckFrameMode(&packet, nullptr) == expectedMode); + ASSERT(dcfRateSelection.computeResponseCtsFrameMode(&packet, nullptr) == expectedMode); + + TestQosRateSelection qosRateSelection; + qosRateSelection.setModeSetForTest(modeSet); + ASSERT(qosRateSelection.computeResponseAckFrameMode(&packet, nullptr) == expectedMode); + ASSERT(qosRateSelection.computeResponseCtsFrameMode(&packet, nullptr) == expectedMode); + auto blockAckMode = qosRateSelection.computeResponseBlockAckFrameMode(&packet, basicBlockAckReq); + if (blockAckMode != expectedBlockAckMode) + throw cRuntimeError("%s Basic BlockAck returned %s, expected %s", testCase.name, blockAckMode->getName(), expectedBlockAckMode->getName()); +} + +class LegacyResponseProbe : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + const LegacyProfileCase cases[] = { + {"a", Mbps(54), Mbps(24), Mbps(54)}, + {"b", Mbps(11), Mbps(11), Mbps(11)}, + {"g(mixed)", Mbps(54), Mbps(24), Mbps(54)}, + }; + for (const auto& testCase : cases) + checkCase(testCase); + std::cout << "Legacy a, b, and g(mixed) DCF/QoS ACK/CTS and Basic BlockAck responses preserved.\n"; + } +}; + +Define_Module(LegacyResponseProbe); + +} // namespace Ieee80211LegacyResponseRateSelection + +%file: test.ned +simple LegacyResponseProbe +{ + parameters: + @class(LegacyResponseProbe); +} + +network TestLegacyResponseRateSelection +{ + submodules: + probe: LegacyResponseProbe; +} + +%inifile: omnetpp.ini +[General] +network = TestLegacyResponseRateSelection +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 + +%contains: stdout +Legacy a, b, and g(mixed) DCF/QoS ACK/CTS and Basic BlockAck responses preserved. diff --git a/tests/module/QosRateSelectionNullModeSetInitialization.test b/tests/module/QosRateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..976921dadc1 --- /dev/null +++ b/tests/module/QosRateSelectionNullModeSetInitialization.test @@ -0,0 +1,54 @@ +%description: +QosRateSelection must reject reaching link-layer initialization without a mode set. + +%file: Test.cc +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" + +using namespace inet; +using namespace inet::ieee80211; + +namespace QosRateSelectionNullModeSetInitialization { + +class NullModeSetQosRateSelection : public QosRateSelection +{ + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) + QosRateSelection::initialize(stage); + } +}; + +Define_Module(NullModeSetQosRateSelection); + +} // namespace QosRateSelectionNullModeSetInitialization + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; + +simple NullModeSetQosRateSelection extends QosRateSelection +{ + parameters: + @class(NullModeSetQosRateSelection); + rateControlModule = ""; + mibModule = ""; +} + +network TestQosRateSelectionNullModeSetInitialization +{ + submodules: + rateSelection: NullModeSetQosRateSelection; +} + +%inifile: omnetpp.ini +[General] +network = TestQosRateSelectionNullModeSetInitialization +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false + +%exitcode: 1 + +%contains-regex: stderr +QosRateSelection module TestQosRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization diff --git a/tests/module/RateSelectionNullModeSetInitialization.test b/tests/module/RateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..8358beb9972 --- /dev/null +++ b/tests/module/RateSelectionNullModeSetInitialization.test @@ -0,0 +1,54 @@ +%description: +RateSelection must reject reaching link-layer initialization without a mode set. + +%file: Test.cc +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" + +using namespace inet; +using namespace inet::ieee80211; + +namespace RateSelectionNullModeSetInitialization { + +class NullModeSetRateSelection : public RateSelection +{ + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) + RateSelection::initialize(stage); + } +}; + +Define_Module(NullModeSetRateSelection); + +} // namespace RateSelectionNullModeSetInitialization + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; + +simple NullModeSetRateSelection extends RateSelection +{ + parameters: + @class(NullModeSetRateSelection); + rateControlModule = ""; + mibModule = ""; +} + +network TestRateSelectionNullModeSetInitialization +{ + submodules: + rateSelection: NullModeSetRateSelection; +} + +%inifile: omnetpp.ini +[General] +network = TestRateSelectionNullModeSetInitialization +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false + +%exitcode: 1 + +%contains-regex: stderr +RateSelection module TestRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index a2fcd996b6f..6c91d3a88f1 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -53,7 +53,7 @@ class TestIeee80211Receiver : public Ieee80211Receiver class TestIeee80211Radio : public Ieee80211Radio { public: - void setup(Ieee80211Transmitter *tx, Ieee80211Receiver *rx) + void setup(ITransmitter *tx, IReceiver *rx) { transmitter = tx; receiver = rx; @@ -90,6 +90,10 @@ class TestRateSelectionAccessor : public ieee80211::RateSelection static void notifyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { rs->applyModeSet(ms); } + static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } }; class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection @@ -102,6 +106,10 @@ class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection static void notifyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { rs->applyModeSet(ms); } + static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } }; class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase @@ -494,24 +502,24 @@ rateSelection->par("multicastFrameBitrate").setDoubleValue(6e6); rateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); rateSelection->par("controlFrameBitrate").setDoubleValue(6e6); rateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); -rateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelection->par("responseCtsFrameBitrate").setDoubleValue(-1); rateSelection->finalizeParameters(); rateSelection->buildInside(); -TestRateSelectionAccessor::notifyModeSet(rateSelection, erpModeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == erpModeSet); +TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); -ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection) == nullptr); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); ASSERT(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection) == erpModeSet->getMode(Mbps(24))); // Dynamically switch rateSelection to HT mode set -TestRateSelectionAccessor::notifyModeSet(rateSelection, modeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == modeSet); +TestRateSelectionAccessor::applyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); @@ -519,7 +527,7 @@ ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMod ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); -ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection) == nullptr); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); // Also verify HT-specific fixed mode configured with bandwidth, stream, and guard interval @@ -534,14 +542,15 @@ rateSelectionHt->par("multicastFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("mgmtFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("controlFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("responseAckFrameBitrate").setDoubleValue(6e6); -rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6.5e6); rateSelectionHt->finalizeParameters(); rateSelectionHt->buildInside(); -TestRateSelectionAccessor::notifyModeSet(rateSelectionHt, modeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelectionHt) == modeSet); +TestRateSelectionAccessor::applyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelectionHt) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt))); ASSERT(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt) == short65); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelectionHt))); auto *qosRateSelection = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.QosRateSelection")->create("qosRateSelection", this)); qosRateSelection->par("rateControlModule").setStringValue(""); @@ -552,18 +561,18 @@ qosRateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("controlFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("responseBlockAckFrameBitrate").setDoubleValue(6e6); -qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(-1); qosRateSelection->finalizeParameters(); qosRateSelection->buildInside(); -TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, erpModeSet); -ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == erpModeSet); +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); -TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, modeSet); -ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == modeSet); +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); From 0c1f9a26b7a96988f7d9f1484cf76f681ba7f58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 02:22:16 +0200 Subject: [PATCH 50/91] fix(ieee80211): negotiate HT Greenfield capability and fall back to HT-mixed for mixed peers - Mode & physical layer introspection: - Add virtual isHtGreenfield() query to IIeee80211Mode and override it in Ieee80211HtMode to identify Greenfield preamble modes. - Track htGreenfieldSupported in Ieee80211ModeSet constructor and expose isHtGreenfieldSupported() accessor. - Expose findHtMixedMode() on Ieee80211ModeSet to resolve the HT-mixed equivalent of any HT mode based on precomputed response tables. - MIB & directional capability negotiation: - Populate localHtCapabilities.greenfield from modeSet->isHtGreenfieldSupported() in Ieee80211Mib::updateLocalHtCapabilities. - Add receiverGreenfield to Ieee80211HtDirectionalCapabilities and populate directional flags during negotiateHtCapabilities(). - Rate selection peer filtering & HT-mixed fallback: - In isCompatibleHtMode(), reject candidate Greenfield modes when the negotiated receiver did not advertise Greenfield support. - In selectPeerCompatibleMode(), dynamically map Greenfield candidate modes to their legal HT-mixed equivalents, allowing Greenfield stations to communicate with mixed-format peers at high throughput before falling back to legacy rates. - Testing: - Update tests/unit/Ieee80211HtCapabilities_1.test with directional Greenfield capability assertions. - Add tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test verifying bidirectional unicast data delivery and dynamic HT-mixed frame format selection in a heterogeneous Greenfield/Mixed BSS. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 1 + .../Ieee80211PeerModeSelection.cc | 17 +- .../ieee80211/mib/Ieee80211HtCapabilities.h | 5 +- .../wireless/ieee80211/mode/IIeee80211Mode.h | 4 +- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 1 + .../ieee80211/mode/Ieee80211ModeSet.cc | 10 + .../ieee80211/mode/Ieee80211ModeSet.h | 4 + ...80211HtHeterogeneousGreenfieldRuntime.test | 199 ++++++++++++++++++ tests/unit/Ieee80211HtCapabilities_1.test | 3 + 9 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index a9454bcd533..b1a6ec212a1 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -143,6 +143,7 @@ void Ieee80211Mac::updateLocalHtCapabilities(bool reconfiguration) // 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.greenfield = modeSet->isHtGreenfieldSupported(); localHtCapabilities.txMcsNss = Ieee80211HtMcsNssMap(); for (int mcs = 0; mcs < 32; mcs++) { if (localHtCapabilities.rxMcsSupported[mcs]) { diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 7d9751369ae..2b930bc8a40 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -86,6 +86,11 @@ static bool isCompatibleHtMode(const IIeee80211Mode *mode, const Ieee80211Mib::P bandwidth > operation.operatingChannelWidth) return false; + // IEEE Std 802.11-2024, 19.1.1 and 19.1.4: an HT-greenfield PPDU may only + // be transmitted to a receiver that advertised HT-greenfield support. + if (mode->isHtGreenfield() && !receiverCapabilities.receiverGreenfield) + return false; + // IEEE Std 802.11-2024, 10.17 and Table 9-224: a short guard interval // is usable only when the receiver advertised it for this channel width. if (mode->isHtShortGuardInterval()) { @@ -137,14 +142,20 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, if (!htEligible || operation == nullptr || peerHtState == nullptr || !peerHtState->valid || !peerHtState->negotiatedCapabilities || !peerHtState->negotiatedCapabilities->localTxPeerRx.valid) return getLegacyFallback(modeSet, mode, peerAddress); - if (isCompatibleHtMode(mode, peerHtState, *operation)) - return mode; + + const IIeee80211Mode *effectiveMode = mode; + if (effectiveMode->isHtGreenfield() && !peerHtState->negotiatedCapabilities->localTxPeerRx.receiverGreenfield) + effectiveMode = modeSet->findHtMixedMode(mode); + if (effectiveMode != nullptr && isCompatibleHtMode(effectiveMode, peerHtState, *operation)) + return effectiveMode; auto candidateBitrate = mode->getDataMode()->getNetBitrate(); const IIeee80211Mode *bestMode = nullptr; for (int i = 0; i < modeSet->getNumModes(); i++) { const auto *candidate = modeSet->getMode(i); - if (candidate->getHtMcsIndex() < 0 || + if (candidate->isHtGreenfield() && !peerHtState->negotiatedCapabilities->localTxPeerRx.receiverGreenfield) + candidate = modeSet->findHtMixedMode(candidate); + if (candidate == nullptr || candidate->getHtMcsIndex() < 0 || candidate->getDataMode()->getNetBitrate() > candidateBitrate || !isCompatibleHtMode(candidate, peerHtState, *operation)) continue; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index 5d58918bc85..fad22e8e288 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -88,6 +88,7 @@ struct Ieee80211HtDirectionalCapabilities Ieee80211HtMcsNssMap mcsNss; std::array supportedMcs = {}; bool receiverLdpc = false; + bool receiverGreenfield = false; bool receiverShortGi20 = false; bool receiverShortGi40 = false; int receiverMaxAmpduLengthExponent = 0; @@ -130,9 +131,11 @@ inline Ieee80211NegotiatedHtCapabilities negotiateHtCapabilities(const Ieee80211 negotiated.localRxPeerTx.mcsNss.maxMcsPerNss[nss] = mcs; } } - // LDPC and short-GI bits advertise receiver capability, so they are directional. + // LDPC, Greenfield, and short-GI bits advertise receiver capability, so they are directional. negotiated.localTxPeerRx.receiverLdpc = peer.ldpc; negotiated.localRxPeerTx.receiverLdpc = local.ldpc; + negotiated.localTxPeerRx.receiverGreenfield = peer.greenfield; + negotiated.localRxPeerTx.receiverGreenfield = local.greenfield; negotiated.localTxPeerRx.receiverShortGi20 = peer.shortGi20; negotiated.localRxPeerTx.receiverShortGi20 = local.shortGi20; negotiated.localTxPeerRx.receiverShortGi40 = peer.shortGi40; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index 9af016b4032..cea690cf72d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -65,6 +65,9 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject // Returns whether this mode uses the optional 400 ns HT guard interval. // Non-HT modes deliberately report false. virtual bool isHtShortGuardInterval() const = 0; + // Returns whether this mode uses the HT Greenfield preamble format. + // Non-HT modes and HT-mixed modes report false. + virtual bool isHtGreenfield() const { return false; } virtual int getLegacyCwMin() const = 0; virtual int getLegacyCwMax() const = 0; virtual const char *getName() const = 0; @@ -94,4 +97,3 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 5e5d43b6fa3..b35858ad69d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -248,6 +248,7 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual const Ieee80211HtDataMode *getDataMode() const override { return dataMode; } virtual int getHtMcsIndex() const override { return dataMode->getMcsIndex(); } virtual bool isHtShortGuardInterval() const override { return dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT; } + virtual bool isHtGreenfield() const override { return preambleMode->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; } virtual const Ieee80211HtPreambleMode *getPreambleMode() const override { return preambleMode; } virtual const Ieee80211HtSignalMode *getHeaderMode() const override { return preambleMode->getSignalMode(); } virtual const Ieee80211OfdmSignalMode *getLegacySignalMode() const { return preambleMode->getLegacySignalMode(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 426ef2a2a68..ab0ffd869cb 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -854,6 +854,8 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en htSupportedChannelWidths.insert(bandwidth); if (entry.mode->isHtShortGuardInterval()) htShortGuardIntervalChannelWidths.insert(bandwidth); + if (entry.mode->isHtGreenfield()) + htGreenfieldSupported = true; } if (htOperationSupported) { for (int mcsIndex = 0; mcsIndex < 8; mcsIndex++) @@ -1283,6 +1285,14 @@ const IIeee80211Mode *Ieee80211ModeSet::getNonHtControlResponseMode(const IIeee8 return result; } +const IIeee80211Mode *Ieee80211ModeSet::findHtMixedMode(const IIeee80211Mode *mode) const +{ + if (mode == nullptr) + return nullptr; + auto it = htMixedControlResponseModes.find(mode); + return it != htMixedControlResponseModes.end() ? it->second : nullptr; +} + const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) { for (size_t index = 0; index < (&modeSets)->size(); index++) { diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index 23d96eaa08d..ad3842ba5c9 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -53,6 +53,7 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject std::array htMcsMandatory = {}; std::set htSupportedChannelWidths; std::set htShortGuardIntervalChannelWidths; + bool htGreenfieldSupported = false; bool htOperationSupported = false; // Entries are selectable modes; supportedEntries also contains immutable PHY capabilities needed for mandatory control responses. const std::vector supportedEntries; @@ -95,6 +96,7 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const std::set& getHtSupportedChannelWidths() const { return htSupportedChannelWidths; } const std::set& getHtShortGuardIntervalChannelWidths() const { return htShortGuardIntervalChannelWidths; } bool isHtShortGuardIntervalSupported(Hz bandwidth) const { return htShortGuardIntervalChannelWidths.count(bandwidth) != 0; } + bool isHtGreenfieldSupported() const { return htGreenfieldSupported; } // containsMode() covers entries selectable as the persistent operating mode // (for example through Ieee80211Transmitter::setMode()). supportsMode() @@ -136,6 +138,8 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject // 2.4 GHz HT modes are converted to non-HT responses. With mandatory=false, // the input bitrate is used as a ceiling for selecting that non-HT mode. const IIeee80211Mode *getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory = true) const; + // Returns the HT-mixed equivalent mode for an HT mode, or nullptr if not an HT mode or not mapped. + const IIeee80211Mode *findHtMixedMode(const IIeee80211Mode *mode) const; static const Ieee80211ModeSet *findModeSet(const char *mode); static const Ieee80211ModeSet *getModeSet(const char *mode); diff --git a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test new file mode 100644 index 00000000000..37040b28817 --- /dev/null +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -0,0 +1,199 @@ +%description: +Verifies bidirectional unicast delivery and dynamic PPDU format selection in a +heterogeneous BSS with one n(greenfield-2.4Ghz) station, an n(mixed-2.4Ghz) +access point, and an n(mixed-2.4Ghz) station. Because the AP is mixed-only, the +Greenfield station must recognize that the peer cannot receive Greenfield PPDUs +and dynamically select the compatible HT-mixed format for uplink transmissions. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtHeterogeneousGreenfieldRuntime { + +static OPP_THREAD_LOCAL int greenfieldStaMixedDataCount = 0; +static OPP_THREAD_LOCAL int greenfieldStaGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int mixedStaDataCount = 0; +static OPP_THREAD_LOCAL int apForwardedDataCount = 0; +static OPP_THREAD_LOCAL int nonHtAckCount = 0; + +class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener +{ + using cListener::finish; + + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + bool isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; + bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + + std::string fullPath = getFullPath(); + bool isHost0 = fullPath.find("host[0]") != std::string::npos; + bool isHost1 = fullPath.find("host[1]") != std::string::npos; + bool isAp = fullPath.find("ap") != std::string::npos; + + if (dynamicPtrCast(macHeader)) { + if (isHost0) { + if (isHtGreenfield) + greenfieldStaGreenfieldDataCount++; + else if (isHtMixed) + greenfieldStaMixedDataCount++; + } + else if (isHost1 && isHtMixed) + mixedStaDataCount++; + else if (isAp && isHtMixed) + apForwardedDataCount++; + } + if (dynamicPtrCast(macHeader) && mode == nullptr) + nonHtAckCount++; + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (greenfieldStaGreenfieldDataCount > 0) + throw cRuntimeError("Greenfield station sent %d HT-Greenfield frame(s) to mixed-only AP", greenfieldStaGreenfieldDataCount); + if (greenfieldStaMixedDataCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected bidirectional HT-mixed data and non-HT ACK exchanges"); + std::cout << "Observed " << greenfieldStaMixedDataCount << " GF-STA HT-mixed data, " + << mixedStaDataCount << " Mixed-STA data, " + << apForwardedDataCount << " AP forwarded data, 0 Greenfield data, and " + << nonHtAckCount << " non-HT ACK response(s).\n"; + } +}; + +Define_Module(HeterogeneousHtCheckingRadio); + +} // namespace Ieee80211HtHeterogeneousGreenfieldRuntime + +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module HeterogeneousHtCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HeterogeneousHtCheckingRadio); +} + +network TestHeterogeneousHtGreenfield +{ + parameters: + int numHosts = default(2); + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[numHosts]: WirelessHost { + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + ap: AccessPoint { + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + } +} + +%inifile: omnetpp.ini + +[General] +network = TestHeterogeneousHtGreenfield +abstract = false +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 600m +**.constraintAreaMaxY = 400m +**.constraintAreaMaxZ = 0m + +**.arp.typename = "GlobalArp" + +*.ap.wlan[*].address = "10:00:00:00:00:00" +*.host[*].**.mgmt.accessPointAddress = "10:00:00:00:00:00" + +*.ap.mobility.typename = "StationaryMobility" +*.ap.mobility.initialX = 250m +*.ap.mobility.initialY = 200m +*.host[*].mobility.typename = "StationaryMobility" +*.host[*].mobility.initialY = 200m +*.host[0].mobility.initialX = 240m +*.host[1].mobility.initialX = 260m + +**.wlan[*].radio.typename = "HeterogeneousHtCheckingRadio" + +[Config HeterogeneousHtRuntime] +abstract = false +sim-time-limit = 2s +*.numHosts = 2 + +# Node configuration: host[0] is Greenfield, AP and host[1] are Mixed-HT +*.host[0].wlan[*].opMode = "n(greenfield-2.4Ghz)" +*.host[0].wlan[*].bitrate = 13Mbps +*.host[1].wlan[*].opMode = "n(mixed-2.4Ghz)" +*.host[1].wlan[*].bitrate = 13Mbps +*.ap.wlan[*].opMode = "n(mixed-2.4Ghz)" +*.ap.wlan[*].bitrate = 13Mbps + +# Bidirectional UDP traffic between host[0] and host[1] through AP +*.host[0].numApps = 2 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[0].app[0].sendInterval = 100ms + +*.host[0].app[1].typename = "UdpSink" +*.host[0].app[1].localPort = 2000 + +*.host[1].numApps = 2 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 + +*.host[1].app[1].typename = "UdpBasicApp" +*.host[1].app[1].destAddresses = "host[0]" +*.host[1].app[1].destPort = 2000 +*.host[1].app[1].messageLength = 100B +*.host[1].app[1].startTime = 150ms +*.host[1].app[1].sendInterval = 100ms + +%extraargs: -c HeterogeneousHtRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index 2f479f16966..298ba7d68f6 100644 --- a/tests/unit/Ieee80211HtCapabilities_1.test +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -20,6 +20,7 @@ local.rxMcsSupported[2] = true; local.rxMcsSupported[3] = true; local.txMcsNss.maxMcsPerNss[0] = 3; local.ldpc = true; +local.greenfield = true; local.shortGi20 = true; local.maxAmpduLengthExponent = 1; @@ -47,6 +48,8 @@ ASSERT(!negotiated.localRxPeerTx.supportedMcs[1]); ASSERT(negotiated.localRxPeerTx.supportedMcs[2]); ASSERT(!negotiated.localTxPeerRx.receiverLdpc); ASSERT(negotiated.localRxPeerTx.receiverLdpc); +ASSERT(!negotiated.localTxPeerRx.receiverGreenfield); +ASSERT(negotiated.localRxPeerTx.receiverGreenfield); ASSERT(negotiated.localTxPeerRx.receiverMaxAmpduLengthExponent == 3); ASSERT(negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent == 1); From cc734a5ca1c719ac8bcd9c7fcb3a8fdc4015ad2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 02:37:23 +0200 Subject: [PATCH 51/91] fix(linklayer): accept supported HT-mixed CTS responses in Greenfield peer mode selection - Rate selection peer filtering: - In selectPeerCompatibleMode(), use modeSet->supportsMode(mode) instead of modeSet->containsMode(mode) to validate candidate modes. - While containsMode() verifies persistent selectable operating modes (which for Greenfield mode sets only include Greenfield PPDUs), supportsMode() correctly covers supported supplementary modes, such as mandatory HT-mixed CTS responses required by IEEE 802.11-2024 subclauses 10.6.6.1 and 10.6.6.5.7. - This prevents runtime simulation aborts when Greenfield stations respond to HT RTS frames in infrastructure networks. - Testing: - Add assertions in tests/unit/Ieee80211PeerModeSelection_1.test verifying that selectPeerCompatibleMode() accepts HT-mixed CTS modes for n(greenfield-2.4Ghz) mode sets while still rejecting unsupported MCS indices. - Update tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test with RTS/CTS enabled (rtsThreshold = 1B) to verify end-to-end HT-mixed CTS generation and transmission by Greenfield stations. --- .../Ieee80211PeerModeSelection.cc | 4 ++-- ...80211HtHeterogeneousGreenfieldRuntime.test | 11 ++++++++--- tests/unit/Ieee80211PeerModeSelection_1.test | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 2b930bc8a40..24e56b202f0 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -135,8 +135,8 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, return mode; if (modeSet == nullptr) throw cRuntimeError("Cannot select a peer-compatible HT mode without an IEEE 802.11 mode set"); - if (!modeSet->containsMode(mode)) - throw cRuntimeError("HT mode '%s' is not contained in IEEE 802.11 mode set '%s'", + if (!modeSet->supportsMode(mode)) + throw cRuntimeError("HT mode '%s' is not supported by IEEE 802.11 mode set '%s'", mode->getName(), modeSet->getName()); if (!htEligible || operation == nullptr || peerHtState == nullptr || !peerHtState->valid || diff --git a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test index 37040b28817..e753cf7ce9e 100644 --- a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -22,6 +22,7 @@ namespace Ieee80211HtHeterogeneousGreenfieldRuntime { static OPP_THREAD_LOCAL int greenfieldStaMixedDataCount = 0; static OPP_THREAD_LOCAL int greenfieldStaGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int greenfieldStaMixedCtsCount = 0; static OPP_THREAD_LOCAL int mixedStaDataCount = 0; static OPP_THREAD_LOCAL int apForwardedDataCount = 0; static OPP_THREAD_LOCAL int nonHtAckCount = 0; @@ -72,6 +73,8 @@ class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener else if (isAp && isHtMixed) apForwardedDataCount++; } + if (dynamicPtrCast(macHeader) && isHtMixed && isHost0) + greenfieldStaMixedCtsCount++; if (dynamicPtrCast(macHeader) && mode == nullptr) nonHtAckCount++; } @@ -81,9 +84,10 @@ class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener Ieee80211Radio::finish(); if (greenfieldStaGreenfieldDataCount > 0) throw cRuntimeError("Greenfield station sent %d HT-Greenfield frame(s) to mixed-only AP", greenfieldStaGreenfieldDataCount); - if (greenfieldStaMixedDataCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) - throw cRuntimeError("Expected bidirectional HT-mixed data and non-HT ACK exchanges"); + if (greenfieldStaMixedDataCount == 0 || greenfieldStaMixedCtsCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected bidirectional HT-mixed data, GF-STA HT-mixed CTS responses, and non-HT ACK exchanges"); std::cout << "Observed " << greenfieldStaMixedDataCount << " GF-STA HT-mixed data, " + << greenfieldStaMixedCtsCount << " GF-STA HT-mixed CTS, " << mixedStaDataCount << " Mixed-STA data, " << apForwardedDataCount << " AP forwarded data, 0 Greenfield data, and " << nonHtAckCount << " non-HT ACK response(s).\n"; @@ -156,6 +160,7 @@ seed-set = 0 *.host[1].mobility.initialX = 260m **.wlan[*].radio.typename = "HeterogeneousHtCheckingRadio" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B [Config HeterogeneousHtRuntime] abstract = false @@ -196,4 +201,4 @@ sim-time-limit = 2s %extraargs: -c HeterogeneousHtRuntime %contains-regex: stdout -Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). +Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* GF-STA HT-mixed CTS, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index aa7a8ea4b89..aba47a02d21 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -187,6 +187,25 @@ ASSERT(firstSelection == secondSelection); ASSERT(firstSelection->getHtMcsIndex() == 8); ASSERT(!firstSelection->isHtShortGuardInterval()); +// Greenfield mode set supports HT-mixed control responses (e.g. CTS) per +// IEEE 802.11-2024 10.6.6.5.7 even though HT-mixed modes are not selectable entries. +const auto *greenfieldModeSet = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"); +const auto *htMixedCtsMode = mcs0Long; // htMcs0BW20MHz mixed format +ASSERT(!greenfieldModeSet->containsMode(htMixedCtsMode)); +ASSERT(greenfieldModeSet->supportsMode(htMixedCtsMode)); +auto greenfieldPeer = makePeerState({0}, {MHz(20)}, MHz(20)); +const auto *greenfieldCtsSelection = selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, htMixedCtsMode, peer); +ASSERT(greenfieldCtsSelection == htMixedCtsMode); + +bool outsideGreenfieldModeRejected = false; +try { + selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, modeOutsideSet, peer); +} +catch (const cRuntimeError&) { + outsideGreenfieldModeRejected = true; +} +ASSERT(outsideGreenfieldModeRejected); + EV << "Peer-compatible HT mode selection checks verified.\n"; %contains: stdout From af9b0adc3c14ad59c58f0083087d6fa4511fd86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 13:11:49 +0200 Subject: [PATCH 52/91] test(linklayer): add unit tests for Greenfield receiver capability and HT-mixed fallback - Peer mode selection testing: - Add optional greenfield receiver capability parameter to makePeerState() helper in Ieee80211PeerModeSelection_1.test. - Verify selectPeerCompatibleMode() selects HT-Greenfield modes when communicating with a Greenfield-capable peer. - Verify isCompatibleHtMode() rejects HT-Greenfield modes when the destination station did not advertise Greenfield reception, causing selectPeerCompatibleMode() to fall back to the compatible HT-mixed equivalent mode. - Verify candidate HT-mixed modes remain unaffected regardless of the destination station's Greenfield reception capability. --- tests/unit/Ieee80211PeerModeSelection_1.test | 29 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index aba47a02d21..2f26e953fe6 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -32,7 +32,7 @@ struct TestPeer : Ieee80211Mib::PeerHtState { }; static TestPeer makePeerState(std::initializer_list mcsIndexes, - std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false) + std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false, bool greenfield = false) { TestPeer state; auto capabilities = std::make_shared(); @@ -46,6 +46,7 @@ static TestPeer makePeerState(std::initializer_list mcsIndexes, receiverCapabilities.supportedChannelWidths.insert(bandwidth); receiverCapabilities.receiverShortGi20 = shortGi20; receiverCapabilities.receiverShortGi40 = shortGi40; + receiverCapabilities.receiverGreenfield = greenfield; state.operation.operatingChannelWidth = operatingChannelWidth; return state; } @@ -194,18 +195,40 @@ const auto *htMixedCtsMode = mcs0Long; // htMcs0BW20MHz mixed format ASSERT(!greenfieldModeSet->containsMode(htMixedCtsMode)); ASSERT(greenfieldModeSet->supportsMode(htMixedCtsMode)); auto greenfieldPeer = makePeerState({0}, {MHz(20)}, MHz(20)); -const auto *greenfieldCtsSelection = selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, htMixedCtsMode, peer); +const auto *greenfieldCtsSelection = selectForTest(greenfieldModeSet, &greenfieldPeer, htMixedCtsMode, peer); ASSERT(greenfieldCtsSelection == htMixedCtsMode); bool outsideGreenfieldModeRejected = false; try { - selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, modeOutsideSet, peer); + selectForTest(greenfieldModeSet, &greenfieldPeer, modeOutsideSet, peer); } catch (const cRuntimeError&) { outsideGreenfieldModeRejected = true; } ASSERT(outsideGreenfieldModeRejected); +const auto *mcs0Greenfield = findHtMode(greenfieldModeSet, 0, MHz(20), Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(mcs0Greenfield != nullptr); +ASSERT(mcs0Greenfield->isHtGreenfield()); + +// When destination advertises Greenfield reception, HT-Greenfield candidate mode is selected. +auto greenfieldCapablePeer = makePeerState({0}, {MHz(20)}, MHz(20), false, false, true); +const auto *greenfieldSelection = selectForTest(greenfieldModeSet, &greenfieldCapablePeer, mcs0Greenfield, peer); +ASSERT(greenfieldSelection == mcs0Greenfield); +ASSERT(greenfieldSelection->isHtGreenfield()); + +// When destination did not advertise Greenfield reception, isCompatibleHtMode rejects Greenfield mode, +// falling back to the legal HT-mixed equivalent mode. +auto mixedOnlyDestinationPeer = makePeerState({0}, {MHz(20)}, MHz(20), false, false, false); +const auto *mixedFallbackSelection = selectForTest(greenfieldModeSet, &mixedOnlyDestinationPeer, mcs0Greenfield, peer); +ASSERT(mixedFallbackSelection == htMixedCtsMode); +ASSERT(!mixedFallbackSelection->isHtGreenfield()); + +// HT-mixed mode candidates remain unaffected regardless of Greenfield receiver capability. +const auto *mixedCandidateSelection = selectForTest(modeSet, &mixedOnlyDestinationPeer, mcs0Long, peer); +ASSERT(mixedCandidateSelection == mcs0Long); +ASSERT(!mixedCandidateSelection->isHtGreenfield()); + EV << "Peer-compatible HT mode selection checks verified.\n"; %contains: stdout From 3cbb43ba4e6ac29657ab9611b2ac9304148e271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 13:58:06 +0200 Subject: [PATCH 53/91] fix(physicallayer): fix transmission duration decomposition for Short GI HT and VHT modes - Physical layer transmission duration decomposition: - In Ieee80211Transmitter::createTransmission(), compute dataDuration using transmissionMode->getDataDuration(B(phyHeader->getLengthField())) instead of raw transmissionMode->getDataMode()->getDuration(...). - For HT mixed format and VHT modes configured with Short Guard Interval (Short GI / 400 ns), standard IEEE 802.11 symbol-boundary rounding (IEEE Std 802.11-2024, Eq. 19-90 and Eq. 21-109) rounds data airtime up to the 4 us symbol boundary. - Comparing duration == preambleDuration + dataDuration now correctly evaluates to true for HT/VHT Short GI transmissions, setting headerDuration to zero (since SIG fields are included in the preamble) and preventing negative dataDuration calculation and runtime simulation errors. - For legacy PHY modes with standalone PLCP headers (OFDM, ERP, DSSS, HR-DSSS), duration == preambleDuration + dataDuration remains false, preserving the header duration allocation. - Testing: - Update assertTransmitterDurationDecomposition() helper in tests/unit/Ieee80211HtGreenfield_1.test to verify duration decomposition with getDataDuration() and accept zero-header HT/VHT modes. - Add test assertions covering VHT Short GI, HT Mixed Short GI, and HT Greenfield Short GI modes with both 0-byte and 64-byte payloads. --- .../packetlevel/Ieee80211Transmitter.cc | 7 ++--- tests/unit/Ieee80211HtGreenfield_1.test | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index a0763a09c7b..b8b0677e049 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -175,14 +175,13 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); - const simtime_t modeledDataDuration = transmissionMode->getDataMode()->getDuration(B(phyHeader->getLengthField())); + const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); // HT/VHT include their SIG fields in the PHY preamble duration, so their - // mode duration is exactly preamble + modeled data. Other PHYs expose a + // mode duration is exactly preamble + data duration. Other PHYs expose a // separate header; their residual data interval may also include a trailing // signal extension (ERP). Keep that extension in chronological data time. - const bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + const bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || preambleDuration + headerDuration + dataDuration != duration) throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); diff --git a/tests/unit/Ieee80211HtGreenfield_1.test b/tests/unit/Ieee80211HtGreenfield_1.test index 11a5efa886f..d6a515de3e8 100644 --- a/tests/unit/Ieee80211HtGreenfield_1.test +++ b/tests/unit/Ieee80211HtGreenfield_1.test @@ -71,19 +71,19 @@ static bool rejectsLegacyConfiguredCts(const Ieee80211ModeSet *modeSet, const II } } -static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode) +static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode, b dataLength = B(0)) { - auto duration = mode->getDuration(B(0)); + auto duration = mode->getDuration(dataLength); auto preambleDuration = mode->getPreambleMode()->getDuration(); - auto modeledDataDuration = mode->getDataMode()->getDuration(B(0)); - bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + auto modeledDataDuration = mode->getDataMode()->getDuration(dataLength); + auto dataDuration = mode->getDataDuration(dataLength); + bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; auto headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : mode->getHeaderMode()->getDuration(); - auto dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; ASSERT(preambleDuration >= SIMTIME_ZERO); ASSERT(headerDuration >= SIMTIME_ZERO); ASSERT(dataDuration >= SIMTIME_ZERO); ASSERT(preambleDuration + headerDuration + dataDuration == duration); - if (dynamic_cast(mode) != nullptr) + if (dynamic_cast(mode) != nullptr || dynamic_cast(mode) != nullptr) ASSERT(headerDuration == SIMTIME_ZERO); if (dynamic_cast(mode) != nullptr) ASSERT(dataDuration > modeledDataDuration); @@ -326,8 +326,21 @@ ASSERT(vhtMixed5GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_5GHZ ASSERT(vhtMixed2GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_2_4GHZ); ASSERT(vhtMixed5GhzMode->getDuration(B(0)) == vhtMixed5GhzMode->getPreambleMode()->getDuration() + vhtMixed5GhzMode->getDataMode()->getDuration(B(0))); ASSERT(vhtMixed5GhzMode->getDataMode()->getDuration(B(0)) >= SIMTIME_ZERO); -assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6))); -assertTransmitterDurationDecomposition(vhtMixed5GhzMode); +auto vhtShortGi5GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6)), B(0)); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6)), B(64)); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode, B(0)); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode, B(64)); +assertTransmitterDurationDecomposition(vhtShortGi5GhzMode, B(0)); +assertTransmitterDurationDecomposition(vhtShortGi5GhzMode, B(64)); +assertTransmitterDurationDecomposition(mixedMode, B(0)); +assertTransmitterDurationDecomposition(mixedMode, B(64)); +assertTransmitterDurationDecomposition(greenfieldMode, B(0)); +assertTransmitterDurationDecomposition(greenfieldMode, B(64)); auto erpMixedProfile = Ieee80211ModeSet::getModeSet("g(mixed)"); auto erpOnlyProfile = Ieee80211ModeSet::getModeSet("g(erp)"); From fb218b88c39bf1dd58aaab77c1a14f563b0a28b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 54/91] physicallayer: preserve scalar signal phase durations Scalar signal models forwarded header and data durations to their base constructor in reverse order. Preserve chronological phase boundaries for both transmission and reception analog models. Add direct TX/RX coverage with distinct preamble, header and data durations. --- .../scalar/ScalarSignalAnalogModel.cc | 2 +- tests/unit/ScalarSignalAnalogModel_1.test | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/unit/ScalarSignalAnalogModel_1.test diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc index 459463d7c98..4d0a86c78c7 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc @@ -12,7 +12,7 @@ namespace inet { namespace physicallayer { ScalarSignalAnalogModel::ScalarSignalAnalogModel(const simtime_t preambleDuration, simtime_t headerDuration, simtime_t dataDuration, Hz centerFrequency, Hz bandwidth, W power) : - NarrowbandSignalAnalogModel(preambleDuration, dataDuration, headerDuration, centerFrequency, bandwidth), + NarrowbandSignalAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, bandwidth), power(power) { } diff --git a/tests/unit/ScalarSignalAnalogModel_1.test b/tests/unit/ScalarSignalAnalogModel_1.test new file mode 100644 index 00000000000..1b7e85c256a --- /dev/null +++ b/tests/unit/ScalarSignalAnalogModel_1.test @@ -0,0 +1,26 @@ +%description: +Scalar transmission and reception analog models preserve distinct chronological +preamble, header, and data durations when forwarding to their shared base. + +%includes: +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarTransmissionAnalogModel.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceptionAnalogModel.h" +using namespace inet; +using namespace inet::physicallayer; + +%activity: +const auto preamble = SimTime(16, SIMTIME_US); +const auto header = SimTime(4, SIMTIME_US); +const auto data = SimTime(120, SIMTIME_US); +ScalarTransmissionAnalogModel tx(preamble, header, data, GHz(2.4), MHz(20), mW(1)); +ScalarReceptionAnalogModel rx(preamble, header, data, GHz(2.4), MHz(20), mW(1)); +for (const ISignalAnalogModel *model : {static_cast(&tx), static_cast(&rx)}) { + ASSERT(model->getPreambleDuration() == preamble); + ASSERT(model->getHeaderDuration() == header); + ASSERT(model->getDataDuration() == data); + ASSERT(model->getDuration() == preamble + header + data); +} +EV << "Scalar TX/RX analog phase durations preserved.\n"; + +%contains: stdout +Scalar TX/RX analog phase durations preserved. From 7446fa47c9504113c5e1feddaaf0269fa06eb62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 55/91] ieee80211: restore the timed HT and VHT header phase Using the full PHY prefix as the preamble left HT and VHT transmissions with no timed header phase. Use the mode's chronological duration accessors while retaining the exact total-duration check. Check positive HT header durations in mixed and Greenfield runtime cases, and verify packet and analog phase boundaries across HT, VHT and legacy catalog variants. --- .../packetlevel/Ieee80211Transmitter.cc | 9 +- .../module/Ieee80211HtGreenfieldRuntime.test | 7 +- tests/module/Ieee80211HtMixedRuntime.test | 7 +- tests/module/Ieee80211TransmissionTiming.test | 106 ++++++++++++++++++ 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 tests/module/Ieee80211TransmissionTiming.test diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index b8b0677e049..465940cfefa 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -174,14 +174,9 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); + const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); + const simtime_t headerDuration = transmissionMode->getHeaderDuration(); const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); - // HT/VHT include their SIG fields in the PHY preamble duration, so their - // mode duration is exactly preamble + data duration. Other PHYs expose a - // separate header; their residual data interval may also include a trailing - // signal extension (ERP). Keep that extension in chronological data time. - const bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; - const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || preambleDuration + headerDuration + dataDuration != duration) throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); diff --git a/tests/module/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test index 60d013e74f3..7648df48f17 100644 --- a/tests/module/Ieee80211HtGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -42,9 +42,10 @@ class HtPreambleCheckingRadio : public Ieee80211Radio, public cListener if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) throw cRuntimeError("Invalid preamble/header/data duration decomposition"); - if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || - transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) - throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + if (mode != nullptr && (transmission->getPreambleDuration() != mode->getPreambleDuration() || + transmission->getHeaderDuration() != mode->getHeaderDuration() || transmission->getHeaderDuration() <= SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not preserve the mode's timed header phase"); auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); auto isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; auto isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; diff --git a/tests/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test index cd81007886e..297ae4de410 100644 --- a/tests/module/Ieee80211HtMixedRuntime.test +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -45,9 +45,10 @@ class HtMixedCheckingRadio : public Ieee80211Radio, public cListener if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) throw cRuntimeError("Invalid preamble/header/data duration decomposition"); - if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || - transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) - throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + if (mode != nullptr && (transmission->getPreambleDuration() != mode->getPreambleDuration() || + transmission->getHeaderDuration() != mode->getHeaderDuration() || transmission->getHeaderDuration() <= SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not preserve the mode's timed header phase"); auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; if (dynamicPtrCast(macHeader) && isHtMixed) diff --git a/tests/module/Ieee80211TransmissionTiming.test b/tests/module/Ieee80211TransmissionTiming.test new file mode 100644 index 00000000000..563072ddea0 --- /dev/null +++ b/tests/module/Ieee80211TransmissionTiming.test @@ -0,0 +1,106 @@ +%description: +Verify packet transmitter chronological preamble/header/data durations and analog +model boundaries for HT-mixed, HT-Greenfield, VHT, and legacy modes. + +%file: Test.cc +#include +#include "inet/common/InitStages.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/common/contract/bitlevel/ISignalAnalogModel.h" +using namespace inet; +using namespace inet::physicallayer; +namespace Ieee80211TransmissionTiming { +class TimingRadio : public Ieee80211Radio +{ + public: + void encapsulateForTest(Packet *packet) const { encapsulate(packet); } +}; +Define_Module(TimingRadio); +class TimingProbe : public cSimpleModule +{ + protected: + int numInitStages() const override { return NUM_INIT_STAGES; } + void initialize(int stage) override { + if (stage != INITSTAGE_LAST) + return; + auto radio = check_and_cast(getSimulation()->getModuleByPath("Test.host.wlan[0].radio")); + auto transmitter = check_and_cast(radio->getSubmodule("transmitter")); + int count = 0; + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)", "ac", "a", "b", "g(mixed)"}) { + auto modes = Ieee80211ModeSet::getModeSet(profile); + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + // One antenna suffices; use every one-stream catalog variant. + if (mode->getDataMode()->getNumberOfSpatialStreams() > 1) + continue; + transmitter->setModeSetAndMode(modes, mode); + for (int length : {1, 100, 1500}) { + Packet packet("timing"); + packet.insertAtBack(makeShared(B(length))); + packet.addTag()->setMode(mode); + radio->encapsulateForTest(&packet); + auto transmission = transmitter->createTransmission(radio, &packet, SimTime(1)); + ASSERT(transmission->getPreambleDuration() == mode->getPreambleDuration()); + ASSERT(transmission->getHeaderDuration() == mode->getHeaderDuration()); + ASSERT(transmission->getHeaderDuration() > SIMTIME_ZERO); + ASSERT(transmission->getDataDuration() == mode->getDataDuration(B(length))); + ASSERT(transmission->getDuration() == mode->getDuration(B(length))); + ASSERT(transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() == transmission->getDuration()); + auto analog = transmission->getAnalogModel(); + ASSERT(analog->getPreambleDuration() == transmission->getPreambleDuration()); + ASSERT(analog->getHeaderDuration() == transmission->getHeaderDuration()); + ASSERT(analog->getDataDuration() == transmission->getDataDuration()); + delete transmission; + count++; + } + } + } + ASSERT(count > 100); + std::cout << "HT, VHT, and legacy packet transmission phases verified.\n"; + } +}; +Define_Module(TimingProbe); +} +%file: test.ned +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +module TimingRadio extends Ieee80211Radio +{ + parameters: + @class(TimingRadio); +} +simple TimingProbe +{ + parameters: + @class(TimingProbe); +} +network Test +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: AdhocHost; + probe: TimingProbe; +} +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 +sim-time-limit = 0s +**.host.wlan[0].opMode = "a" +**.host.wlan[0].radio.typename = "TimingRadio" +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 100m +**.constraintAreaMaxY = 100m +**.constraintAreaMaxZ = 0m +%contains: stdout +HT, VHT, and legacy packet transmission phases verified. From c243022f39944f8af3cd5987351edb289808d70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 56/91] ieee80211: bound mandatory multicast rates by the requested rate Optional multicast requests such as 9 and 18 Mb/s could select the fastest mandatory legacy rate and exceed the requested bitrate. Select the highest mandatory legacy rate within the ceiling, preserving exact mandatory requests. Report an error when mandatory legacy modes exist but none meets the bound. Preserve the request when the mandatory legacy set is empty. Cover optional rates, exact requests and both fallback boundaries. --- .../Ieee80211PeerModeSelection.cc | 7 +++++ .../Ieee80211PeerModeSelection.h | 5 ++++ tests/unit/Ieee80211PeerModeSelection_1.test | 26 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 24e56b202f0..4f3f3a80daf 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -20,15 +20,22 @@ const IIeee80211Mode *selectGroupAddressedMode(const Ieee80211ModeSet *modeSet, // IEEE Std 802.11-2024, 10.6.5.1 and 10.6.5.4. The model advertises // mandatory legacy operational modes as its BSS basic legacy rate set. const auto *resolvedMode = modeSet->containsMode(requestedMode) ? requestedMode : modeSet->findCompatibleMode(requestedMode); + bool hasMandatoryLegacyMode = false; const IIeee80211Mode *legacyMode = nullptr; for (const auto *candidate : modeSet->getLegacyOperationalModes()) { if (!modeSet->getIsMandatory(candidate)) continue; + hasMandatoryLegacyMode = true; + if (candidate->getDataMode()->getNetBitrate() > requestedMode->getDataMode()->getNetBitrate()) + continue; if (candidate == resolvedMode) return candidate; if (legacyMode == nullptr || candidate->getDataMode()->getNetBitrate() > legacyMode->getDataMode()->getNetBitrate()) legacyMode = candidate; } + if (legacyMode == nullptr && hasMandatoryLegacyMode) + throw cRuntimeError("No mandatory legacy mode at or below requested group-addressed mode '%s' in mode set '%s'", + requestedMode->getName(), modeSet->getName()); return legacyMode != nullptr ? legacyMode : requestedMode; } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index ee2547fb299..ce2c8974768 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -14,6 +14,11 @@ namespace inet { namespace ieee80211 { +/** + * Selects the fastest mandatory legacy mode at or below the requested bitrate, + * preserving an exact mandatory request. Throws if mandatory legacy modes exist + * but none meets the bound; preserves the request if that mandatory set is empty. + */ INET_API const physicallayer::IIeee80211Mode *selectGroupAddressedMode( const physicallayer::Ieee80211ModeSet *modeSet, const physicallayer::IIeee80211Mode *requestedMode); diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index 2f26e953fe6..d3caab216d5 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -90,6 +90,32 @@ const auto *modeOutsideSet = Ieee80211HtCompliantModes::getCompliantMode(&Ieee80 Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); ASSERT(!modeSet->containsMode(modeOutsideSet)); +// Group-addressed configured optional rates are upper bounds over the basic +// legacy set (modeled by mandatory legacy operational modes). +for (const char *profile : {"a", "g(mixed)"}) { + const auto *groupSet = Ieee80211ModeSet::getModeSet(profile); + for (int rate : {6, 9, 12, 18, 24}) { + const auto *requested = groupSet->getMode(Mbps(rate)); + auto expected = Mbps(rate == 9 ? 6 : rate == 18 ? 12 : rate); + const auto *selected = selectGroupAddressedMode(groupSet, requested); + ASSERT(selected->getDataMode()->getNetBitrate() == expected); + if (rate != 9 && rate != 18) + ASSERT(selected == requested); + } +} +const auto *legacySet = Ieee80211ModeSet::getModeSet("a"); +const auto *six = legacySet->getMode(Mbps(6)); +const auto *nine = legacySet->getMode(Mbps(9)); +Ieee80211ModeSet aboveBound("aboveBound", {{true, nine, true}}, nine, Ieee80211ModeSet::PhyType::OFDM); +bool groupBoundRejected = false; +try { selectGroupAddressedMode(&aboveBound, six); } +catch (const cRuntimeError& error) { + groupBoundRejected = error.getFormattedMessage().find("No mandatory legacy mode at or below") != std::string::npos; +} +ASSERT(groupBoundRejected); +Ieee80211ModeSet noMandatory("noMandatory", {{false, nine, true}}, nine, Ieee80211ModeSet::PhyType::OFDM); +ASSERT(selectGroupAddressedMode(&noMandatory, nine) == nine); + const MacAddress peer("02:00:00:00:00:01"); auto shortGiPeer = makePeerState({8}, {MHz(20)}, MHz(20), true); From 048e202f0a820dbfc4a79f5526d8da5e65039ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 57/91] ieee80211: require unambiguous configured response modes A response bitrate can identify multiple HT MCS, bandwidth, stream-count and guard-interval combinations. Expose matching qualifiers for DCF/QoS ACK and CTS, plus QoS BlockAck, and reject ambiguous configured matches instead of silently depending on catalog order. Mode-set lookup enforces uniqueness only when requested. Automatic responses and other lookup callers retain their existing behavior. Verify qualified CTS conversion, invalid and automatic configurations, and configured multicast ceilings through both production selectors. --- .../mac/rateselection/QosRateSelection.cc | 10 +- .../mac/rateselection/QosRateSelection.ned | 13 ++ .../mac/rateselection/RateSelection.cc | 8 +- .../mac/rateselection/RateSelection.ned | 10 + .../ieee80211/mode/Ieee80211ModeSet.cc | 24 ++- .../ieee80211/mode/Ieee80211ModeSet.h | 9 +- ...ee80211QualifiedResponseRateSelection.test | 189 ++++++++++++++++++ tests/unit/Ieee80211HtModeSet_1.test | 11 + 8 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 tests/module/Ieee80211QualifiedResponseRateSelection.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index aa6bd4b9a3a..7cb25cebcea 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -18,12 +18,12 @@ namespace ieee80211 { using namespace inet::physicallayer; -static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, const char *modulePath) { if (bitrate == -1) return nullptr; try { - auto result = modeSet->getMode(bps(bitrate)); + auto result = modeSet->getMode(bps(bitrate), bandwidth, numSpatialStreams, guardInterval, true); if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); return result; @@ -84,11 +84,11 @@ void QosRateSelection::updateModes() double controlFrameBitrate = par("controlFrameBitrate"); auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate), Hz(par("responseBlockAckFrameBandwidth")), par("responseBlockAckFrameNumSpatialStreams"), par("responseBlockAckFrameGuardInterval"), true); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, Hz(par("responseCtsFrameBandwidth")), par("responseCtsFrameNumSpatialStreams"), par("responseCtsFrameGuardInterval"), getFullPath().c_str()); auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); multicastFrameMode = newMulticastFrameMode; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 9e0ba20b9e4..2358c211da8 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -28,19 +28,32 @@ simple QosRateSelection extends SimpleModule double multicastFrameBitrate @unit(bps) = default(-1bps); + // Configured response rates must identify one selectable PHY mode. Use the matching + // Bandwidth, NumSpatialStreams, and GuardInterval qualifiers to disambiguate rates. + // Unspecified qualifiers are wildcards; ambiguous matches are rejected. Qualifiers + // are ignored for an automatic (-1bps) response. GI must match the modeled mode. // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + double responseAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseAckFrameNumSpatialStreams = default(-1); + double responseAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT BlockAck PPDU. double responseBlockAckFrameBitrate @unit(bps) = default(-1bps); + double responseBlockAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseBlockAckFrameNumSpatialStreams = default(-1); + double responseBlockAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. A configured value must resolve to a // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); + double responseCtsFrameBandwidth @unit(Hz) = default(nan Hz); + int responseCtsFrameNumSpatialStreams = default(-1); + double responseCtsFrameGuardInterval @unit(s) = default(-1s); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index b12c3346a6c..15c4bf2d2db 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -23,12 +23,12 @@ namespace ieee80211 { using namespace inet::physicallayer; -static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, const char *modulePath) { if (bitrate == -1) return nullptr; try { - auto result = modeSet->getMode(bps(bitrate)); + auto result = modeSet->getMode(bps(bitrate), bandwidth, numSpatialStreams, guardInterval, true); if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); return result; @@ -105,9 +105,9 @@ void RateSelection::updateModes() double controlFrameBitrate = par("controlFrameBitrate"); auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, Hz(par("responseCtsFrameBandwidth")), par("responseCtsFrameNumSpatialStreams"), par("responseCtsFrameGuardInterval"), getFullPath().c_str()); auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); // Commit only after every configured mode has been resolved, so a failed diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index ade0ff83e8f..150a480ce89 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -24,15 +24,25 @@ simple RateSelection extends SimpleModule like IRateSelection double multicastFrameBitrate @unit(bps) = default(-1bps); + // Configured response rates must identify one selectable PHY mode. Use the matching + // Bandwidth, NumSpatialStreams, and GuardInterval qualifiers to disambiguate rates. + // Unspecified qualifiers are wildcards; ambiguous matches are rejected. Qualifiers + // are ignored for an automatic (-1bps) response. GI must match the modeled mode. // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + double responseAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseAckFrameNumSpatialStreams = default(-1); + double responseAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. A configured value must resolve to a // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); + double responseCtsFrameBandwidth @unit(Hz) = default(nan Hz); + int responseCtsFrameNumSpatialStreams = default(-1); + double responseCtsFrameGuardInterval @unit(s) = default(-1s); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index ab0ffd869cb..8fb73eacf29 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -1056,13 +1056,14 @@ const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode return nullptr; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); + return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval, requireUnique); } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { + const IIeee80211Mode *result = nullptr; // 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}) { @@ -1079,18 +1080,23 @@ const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams) && guardIntervalMatches) { - return entries[index].mode; + if (!requireUnique) + return mode; + if (result != nullptr && result != mode) + throw cRuntimeError("Ambiguous mode for bitrate range (%g - %g) bps in operation mode '%s'; specify bandwidth, number of spatial streams, and guard interval", + minBitrate.get(), maxBitrate.get(), getName()); + result = mode; } } if (guardInterval >= SIMTIME_ZERO) break; } - return nullptr; + return result; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); + const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval, requireUnique); if (mode == nullptr) throw cRuntimeError("Unknown mode for bitrate %g bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", bitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); @@ -1098,9 +1104,9 @@ const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int n return mode; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval); + const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval, requireUnique); if (mode == nullptr) throw cRuntimeError("Unknown mode for bitrate range (%g - %g) bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", minBitrate.get(), maxBitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index ad3842ba5c9..8b5e1dd8805 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -113,10 +113,11 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject // Pointer lookup is intentionally strict. Use getControlResponseMode() for an explicitly requested response that needs HT-mixed translation. const IIeee80211Mode *findMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getMode(const IIeee80211Mode *mode) const; - const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + // requireUnique rejects multiple matching entries instead of selecting the first. + const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; const IIeee80211Mode *getSlowestMode() const; const IIeee80211Mode *getFastestMode() const; const IIeee80211Mode *getSlowerMode(const IIeee80211Mode *mode) const; diff --git a/tests/module/Ieee80211QualifiedResponseRateSelection.test b/tests/module/Ieee80211QualifiedResponseRateSelection.test new file mode 100644 index 00000000000..61c17a11672 --- /dev/null +++ b/tests/module/Ieee80211QualifiedResponseRateSelection.test @@ -0,0 +1,189 @@ +%description: +Exercise production DCF/QoS configured response resolution, ambiguity rejection, +qualifier preservation for HT-mixed CTS, and optional multicast rate ceilings. + +%file: Test.cc +#include +#include +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211QualifiedResponseRateSelection { + +template +static void checkSelector(Selector& selector, bool qos) +{ + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)"}) { + auto modes = Ieee80211ModeSet::getModeSet(profile); + for (const char *kind : {"Ack", "Cts", "BlockAck"}) { + if (!qos && std::string(kind) == "BlockAck") + continue; + std::string stem = std::string("response") + kind + "Frame"; + auto bitrate = stem + "Bitrate"; + auto bandwidth = stem + "Bandwidth"; + auto streams = stem + "NumSpatialStreams"; + auto gi = stem + "GuardInterval"; + selector.par(bitrate.c_str()).setDoubleValue(13e6); + bool rejected = false; + try { selector.applyModeSet(modes); } + catch (const cRuntimeError& error) { + rejected = error.getFormattedMessage().find("Ambiguous mode") != std::string::npos; + } + ASSERT(rejected); + selector.par(bandwidth.c_str()).setDoubleValue(20e6); + selector.par(gi.c_str()).setDoubleValue(800e-9); + for (int nss : {1, 2}) { + selector.par(streams.c_str()).setIntValue(nss); + selector.applyModeSet(modes); + auto configured = selector.configured(kind); + ASSERT(configured->getHtMcsIndex() == (nss == 1 ? 1 : 8)); + ASSERT(configured->getDataMode()->getBandwidth() == MHz(20)); + ASSERT(configured->getDataMode()->getGuardInterval() == SimTime(800, SIMTIME_NS)); + if (std::string(kind) == "Cts") { + Packet rts("rts"); + rts.addTag()->setMode(configured); + auto response = selector.computeResponseCtsFrameMode(&rts, nullptr); + ASSERT(!response->isHtGreenfield()); + ASSERT(response->getHtMcsIndex() == configured->getHtMcsIndex()); + ASSERT(response->getDataMode()->getBandwidth() == configured->getDataMode()->getBandwidth()); + ASSERT(response->getDataMode()->getNumberOfSpatialStreams() == nss); + ASSERT(response->getDataMode()->getGuardInterval() == configured->getDataMode()->getGuardInterval()); + } + } + // A conflicting explicit GI must not silently choose another tuple. + selector.par(gi.c_str()).setDoubleValue(400e-9); + rejected = false; + try { selector.applyModeSet(modes); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + // Width and short GI also distinguish a configured HT tuple. This + // exercises selection only; packet-level HT40 operation is unsupported. + selector.par(bitrate.c_str()).setDoubleValue(15e6); + selector.par(bandwidth.c_str()).setDoubleValue(40e6); + selector.par(streams.c_str()).setIntValue(1); + selector.applyModeSet(modes); + auto wideShort = selector.configured(kind); + ASSERT(wideShort->getHtMcsIndex() == 0); + ASSERT(wideShort->getDataMode()->getBandwidth() == MHz(40)); + ASSERT(wideShort->isHtShortGuardInterval()); + if (std::string(kind) == "Cts") { + Packet rts("wideShortRts"); + rts.addTag()->setMode(wideShort); + auto response = selector.computeResponseCtsFrameMode(&rts, nullptr); + ASSERT(!response->isHtGreenfield()); + ASSERT(response->getHtMcsIndex() == 0); + ASSERT(response->getDataMode()->getBandwidth() == MHz(40)); + ASSERT(response->getDataMode()->getNumberOfSpatialStreams() == 1); + ASSERT(response->isHtShortGuardInterval()); + } + selector.par(bitrate.c_str()).setDoubleValue(-1); + selector.applyModeSet(modes); // automatic ignores the qualifiers + ASSERT(selector.configured(kind) == nullptr); + selector.par(bandwidth.c_str()).setDoubleValue(NaN); + selector.par(streams.c_str()).setIntValue(-1); + selector.par(gi.c_str()).setDoubleValue(-1); + } + } + for (const char *profile : {"a", "g(mixed)"}) { + for (int rate : {6, 9, 12, 18, 24}) { + selector.par("multicastFrameBitrate").setDoubleValue(rate * 1e6); + selector.applyModeSet(Ieee80211ModeSet::getModeSet(profile)); + auto header = makeShared(); + header->setReceiverAddress(MacAddress::BROADCAST_ADDRESS); + auto selected = selector.groupMode(header); + ASSERT(selected->getDataMode()->getNetBitrate() == Mbps(rate == 9 ? 6 : rate == 18 ? 12 : rate)); + } + } +} + +class DcfProbe : public RateSelection +{ + public: + const IIeee80211Mode *configured(const char *kind) const { return std::string(kind) == "Ack" ? responseAckFrameMode : responseCtsFrameMode; } + const IIeee80211Mode *groupMode(const Ptr& header) { return computeMode(nullptr, header); } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LINK_LAYER) { + checkSelector(*this, false); + std::cout << "DCF qualified responses and multicast ceilings verified.\n"; + } + } +}; +class QosProbe : public QosRateSelection +{ + public: + const IIeee80211Mode *configured(const char *kind) const { return std::string(kind) == "Ack" ? responseAckFrameMode : std::string(kind) == "Cts" ? responseCtsFrameMode : responseBlockAckFrameMode; } + const IIeee80211Mode *groupMode(const Ptr& header) { return computeMode(nullptr, header, nullptr); } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LINK_LAYER) { + checkSelector(*this, true); + std::cout << "QoS qualified responses and multicast ceilings verified.\n"; + } + } +}; +Define_Module(DcfProbe); +Define_Module(QosProbe); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +simple DcfProbe extends RateSelection +{ + parameters: + @class(DcfProbe); + rateControlModule = ""; + mibModule = ""; + multicastFrameBitrate @mutable; + responseAckFrameBitrate @mutable; + responseAckFrameBandwidth @mutable; + responseAckFrameNumSpatialStreams @mutable; + responseAckFrameGuardInterval @mutable; + responseCtsFrameBitrate @mutable; + responseCtsFrameBandwidth @mutable; + responseCtsFrameNumSpatialStreams @mutable; + responseCtsFrameGuardInterval @mutable; +} +simple QosProbe extends QosRateSelection +{ + parameters: + @class(QosProbe); + rateControlModule = ""; + mibModule = ""; + multicastFrameBitrate @mutable; + responseAckFrameBitrate @mutable; + responseAckFrameBandwidth @mutable; + responseAckFrameNumSpatialStreams @mutable; + responseAckFrameGuardInterval @mutable; + responseCtsFrameBitrate @mutable; + responseCtsFrameBandwidth @mutable; + responseCtsFrameNumSpatialStreams @mutable; + responseCtsFrameGuardInterval @mutable; + responseBlockAckFrameBitrate @mutable; + responseBlockAckFrameBandwidth @mutable; + responseBlockAckFrameNumSpatialStreams @mutable; + responseBlockAckFrameGuardInterval @mutable; +} +network Test +{ + submodules: + dcf: DcfProbe; + qos: QosProbe; +} +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 +%contains: stdout +DCF qualified responses and multicast ceilings verified. +%contains: stdout +QoS qualified responses and multicast ceilings verified. diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 00a6bc4967c..d55ad09fdf1 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -87,6 +87,17 @@ for (const auto& expected : optionalMcs) { const auto *htModeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); const auto *vhtOnlyModeSet = Ieee80211ModeSet::getModeSet("ac"); +// 13 Mbps identifies both one-stream MCS 1 and two-stream MCS 8. +bool ambiguousRejected = false; +try { htModeSet->getMode(Mbps(13), MHz(20), -1, SimTime(800, SIMTIME_NS), true); } +catch (const cRuntimeError& error) { + ambiguousRejected = error.getFormattedMessage().find("Ambiguous mode") != std::string::npos; +} +ASSERT(ambiguousRejected); +ASSERT(htModeSet->getMode(Mbps(13), MHz(20), 1, SimTime(800, SIMTIME_NS), true)->getHtMcsIndex() == 1); +ASSERT(htModeSet->getMode(Mbps(13), MHz(20), 2, SimTime(800, SIMTIME_NS), true)->getHtMcsIndex() == 8); +ASSERT(htModeSet->findMode(Mbps(9999), MHz(20), 1, -1, true) == nullptr); + ASSERT(htModeSet->isHtOperationSupported()); ASSERT(!vhtOnlyModeSet->isHtOperationSupported()); From 3b2348a1f019dad3eeec1b7e5bb27ce8a1f0681a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 58/91] ieee80211: document the packet PHY HT40 limitation The HT catalog can represent 40 MHz modes, but the packet-level transmitter and receiver advertise only 20 MHz operation. Clarify that nonzero secondary-channel offsets require PHY width support and are rejected by these providers. --- src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index 35b54143ac6..138515cfa0a 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -27,7 +27,7 @@ simple Ieee80211Mib extends SimpleModule @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 + int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; requires PHY width support. The packet-level Ieee80211Transmitter/Receiver support only 20 MHz and reject nonzero offsets. int htProtectionMode = default(0); // 0=none, 1=nonmember, 2=20 MHz, 3=non-HT mixed displayStringTextFormat = default("Address: {address}{ssidStr}\n{modeStr}{stationTypeStr}{qosStr}{associatedStr}"); @display("i=block/table"); From bea61f5cc2817a390154ea68993a22d3f698d43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 21 Sep 2026 15:21:46 +0200 Subject: [PATCH 59/91] ieee80211: adapt Greenfield coverage to current GI contracts The GI base now coordinates mode transitions explicitly and gates HT data on a committed BSS relationship. Exercise those production paths in the Greenfield fixtures, including infrastructure exchanges and null catalog providers. Match the chronological PHY phases and preserve ownership of catalog preambles in synthetic mode tests. Keep the upstream null-request group-mode fallback while applying the topic rate ceiling to explicit requests. Update the group-selection assertions to cover both contracts. Final-tree validation: debug INET build, 9 focused unit tests, 11 focused module tests, MAC/PHY architecture checks, and TXOP tplx/~tNl fingerprints pass. The graphical tyf fingerprint differs and is not updated. No intermediate commit was built or tested. --- .../Ieee80211PeerModeSelection.cc | 2 +- ...e80211ConfiguredResponseRateSelection.test | 9 ++++---- .../module/Ieee80211HtGreenfieldRuntime.test | 10 +++----- ...80211HtHeterogeneousGreenfieldRuntime.test | 7 +++--- tests/module/Ieee80211HtMixedRuntime.test | 10 ++++---- ...ateSelectionNullModeSetInitialization.test | 23 +++++++++++++++---- ...ateSelectionNullModeSetInitialization.test | 23 +++++++++++++++---- tests/unit/Ieee80211GroupModeSelection_1.test | 14 +++++++---- tests/unit/Ieee80211HtGuardInterval_1.test | 4 ++-- tests/unit/Ieee80211HtModeSet_1.test | 18 +++++++++++---- 10 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 4f3f3a80daf..47c7afdd833 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -26,7 +26,7 @@ const IIeee80211Mode *selectGroupAddressedMode(const Ieee80211ModeSet *modeSet, if (!modeSet->getIsMandatory(candidate)) continue; hasMandatoryLegacyMode = true; - if (candidate->getDataMode()->getNetBitrate() > requestedMode->getDataMode()->getNetBitrate()) + if (requestedMode != nullptr && candidate->getDataMode()->getNetBitrate() > requestedMode->getDataMode()->getNetBitrate()) continue; if (candidate == resolvedMode) return candidate; diff --git a/tests/module/Ieee80211ConfiguredResponseRateSelection.test b/tests/module/Ieee80211ConfiguredResponseRateSelection.test index e8212250c11..c3882d4a761 100644 --- a/tests/module/Ieee80211ConfiguredResponseRateSelection.test +++ b/tests/module/Ieee80211ConfiguredResponseRateSelection.test @@ -7,6 +7,7 @@ non-HT rates and configured HT CTS rates use exact HT-mixed counterparts. #include #include "inet/common/InitStages.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" #include "inet/common/Simsignals.h" #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" @@ -120,11 +121,11 @@ class ConfiguredResponseProbe : public cSimpleModule assertHtMixedResponse("Configured DCF CTS MCS 0 override for MCS 7", dcfRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); assertHtMixedResponse("Configured CTS MCS 0 override for MCS 7", qosRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); - // Exercise the actual signal path with a compatible mode-set transition. + // Exercise the coordinated radio mode-set transition. // The configured data cache must bind to the exact profile object while // the configured response operations remain usable after each refresh. - auto wlan = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0]")); - wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"))); + auto radio = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0].radio")); + radio->setModeSet(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == mixedSourceMode); ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == mixedSourceMode); assertHtMixedResponse("Rebound DCF CTS mixed", dcfRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); @@ -132,7 +133,7 @@ class ConfiguredResponseProbe : public cSimpleModule assertNonHtResponse("Rebound DCF ACK mixed", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); assertNonHtResponse("Rebound QoS ACK mixed", qosRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); assertNonHtResponse("Rebound QoS BlockAck mixed", qosRateSelection->computeResponseBlockAckFrameMode(nullptr, basicBlockAckReq)); - wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"))); + radio->setModeSet(Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)")); ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == sourceMode); ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == sourceMode); assertHtMixedResponse("Rebound DCF CTS Greenfield", dcfRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); diff --git a/tests/module/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test index 7648df48f17..6afe11774f8 100644 --- a/tests/module/Ieee80211HtGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -75,7 +75,7 @@ Define_Module(HtPreambleCheckingRadio); %file: test.ned import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; -import inet.node.inet.AdhocHost; +import inet.examples.wireless.lan80211.Lan80211; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; @@ -85,14 +85,10 @@ module HtPreambleCheckingRadio extends Ieee80211Radio @class(HtPreambleCheckingRadio); } -network TestHtGreenfield +network TestHtGreenfield extends Lan80211 { parameters: - int numHosts = default(2); - submodules: - configurator: Ipv4NetworkConfigurator; - radioMedium: Ieee80211ScalarRadioMedium; - host[numHosts]: AdhocHost; + numHosts = default(2); } %inifile: omnetpp.ini diff --git a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test index e753cf7ce9e..47750463139 100644 --- a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -49,9 +49,10 @@ class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) throw cRuntimeError("Invalid preamble/header/data duration decomposition"); - if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || - transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) - throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + if (mode != nullptr && (transmission->getPreambleDuration() != mode->getPreambleDuration() || + transmission->getHeaderDuration() != mode->getHeaderDuration() || transmission->getHeaderDuration() <= SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not preserve the mode phase durations"); auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); bool isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; diff --git a/tests/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test index 297ae4de410..ccc530ad86d 100644 --- a/tests/module/Ieee80211HtMixedRuntime.test +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -79,7 +79,7 @@ Define_Module(HtMixedCheckingRadio); %file: test.ned import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; -import inet.node.inet.AdhocHost; +import inet.examples.wireless.lan80211.Lan80211; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; @@ -89,12 +89,10 @@ module HtMixedCheckingRadio extends Ieee80211Radio @class(HtMixedCheckingRadio); } -network TestHtMixedRuntime +network TestHtMixedRuntime extends Lan80211 { - submodules: - configurator: Ipv4NetworkConfigurator; - radioMedium: Ieee80211ScalarRadioMedium; - host[2]: AdhocHost; + parameters: + numHosts = default(2); } %inifile: omnetpp.ini diff --git a/tests/module/QosRateSelectionNullModeSetInitialization.test b/tests/module/QosRateSelectionNullModeSetInitialization.test index 976921dadc1..e0f7d8a0175 100644 --- a/tests/module/QosRateSelectionNullModeSetInitialization.test +++ b/tests/module/QosRateSelectionNullModeSetInitialization.test @@ -10,16 +10,24 @@ using namespace inet::ieee80211; namespace QosRateSelectionNullModeSetInitialization { +class NullModeSetProvider : public cSimpleModule, public IIeee80211ModeSetProvider +{ + public: + const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const override { return nullptr; } +}; +Define_Module(NullModeSetProvider); + class NullModeSetQosRateSelection : public QosRateSelection { protected: - virtual void initialize(int stage) override + void initialize(int stage) override { - if (stage == INITSTAGE_LINK_LAYER) + if (stage == INITSTAGE_LOCAL) + ModeSetModuleBase::initialize(stage); + else if (stage == INITSTAGE_LINK_LAYER) QosRateSelection::initialize(stage); } }; - Define_Module(NullModeSetQosRateSelection); } // namespace QosRateSelectionNullModeSetInitialization @@ -27,10 +35,16 @@ Define_Module(NullModeSetQosRateSelection); %file: test.ned import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +simple NullModeSetProvider +{ + @class(NullModeSetProvider); +} + simple NullModeSetQosRateSelection extends QosRateSelection { parameters: @class(NullModeSetQosRateSelection); + modeSetModule = "^.provider"; rateControlModule = ""; mibModule = ""; } @@ -38,6 +52,7 @@ simple NullModeSetQosRateSelection extends QosRateSelection network TestQosRateSelectionNullModeSetInitialization { submodules: + provider: NullModeSetProvider; rateSelection: NullModeSetQosRateSelection; } @@ -51,4 +66,4 @@ record-vector-results = false %exitcode: 1 %contains-regex: stderr -QosRateSelection module TestQosRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization +Configured IEEE 802.11 mode catalog is unavailable diff --git a/tests/module/RateSelectionNullModeSetInitialization.test b/tests/module/RateSelectionNullModeSetInitialization.test index 8358beb9972..e3aa82d74ee 100644 --- a/tests/module/RateSelectionNullModeSetInitialization.test +++ b/tests/module/RateSelectionNullModeSetInitialization.test @@ -10,16 +10,24 @@ using namespace inet::ieee80211; namespace RateSelectionNullModeSetInitialization { +class NullModeSetProvider : public cSimpleModule, public IIeee80211ModeSetProvider +{ + public: + const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const override { return nullptr; } +}; +Define_Module(NullModeSetProvider); + class NullModeSetRateSelection : public RateSelection { protected: - virtual void initialize(int stage) override + void initialize(int stage) override { - if (stage == INITSTAGE_LINK_LAYER) + if (stage == INITSTAGE_LOCAL) + ModeSetModuleBase::initialize(stage); + else if (stage == INITSTAGE_LINK_LAYER) RateSelection::initialize(stage); } }; - Define_Module(NullModeSetRateSelection); } // namespace RateSelectionNullModeSetInitialization @@ -27,10 +35,16 @@ Define_Module(NullModeSetRateSelection); %file: test.ned import inet.linklayer.ieee80211.mac.rateselection.RateSelection; +simple NullModeSetProvider +{ + @class(NullModeSetProvider); +} + simple NullModeSetRateSelection extends RateSelection { parameters: @class(NullModeSetRateSelection); + modeSetModule = "^.provider"; rateControlModule = ""; mibModule = ""; } @@ -38,6 +52,7 @@ simple NullModeSetRateSelection extends RateSelection network TestRateSelectionNullModeSetInitialization { submodules: + provider: NullModeSetProvider; rateSelection: NullModeSetRateSelection; } @@ -51,4 +66,4 @@ record-vector-results = false %exitcode: 1 %contains-regex: stderr -RateSelection module TestRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization +Configured IEEE 802.11 mode catalog is unavailable diff --git a/tests/unit/Ieee80211GroupModeSelection_1.test b/tests/unit/Ieee80211GroupModeSelection_1.test index 8a0697e76ad..7c30f62dafc 100644 --- a/tests/unit/Ieee80211GroupModeSelection_1.test +++ b/tests/unit/Ieee80211GroupModeSelection_1.test @@ -1,6 +1,6 @@ %description: Group selection preserves an equivalent external basic legacy mode, while -retaining active-catalog identity and the existing fastest-basic fallback. +retaining active-catalog identity and bounding fallback by the requested rate. %includes: #include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" @@ -32,14 +32,20 @@ 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, nine) == six); 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); +bool optionalRejected = false; +try { selectGroupAddressedMode(&optional, &externalSix); } +catch (const cRuntimeError&) { optionalRejected = true; } +ASSERT(optionalRejected); GroupModeSet nonOperational({{true, six, false}, {true, twentyFour, true}}, twentyFour); -ASSERT(selectGroupAddressedMode(&nonOperational, &externalSix) == twentyFour); +bool nonOperationalRejected = false; +try { selectGroupAddressedMode(&nonOperational, &externalSix); } +catch (const cRuntimeError&) { nonOperationalRejected = true; } +ASSERT(nonOperationalRejected); // 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); diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 6c91d3a88f1..bf8a478b98f 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -92,7 +92,7 @@ class TestRateSelectionAccessor : public ieee80211::RateSelection } static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { - static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + rs->applyModeSet(ms); } }; @@ -108,7 +108,7 @@ class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection } static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { - static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + rs->applyModeSet(ms); } }; diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index d55ad09fdf1..7b12d02df28 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -39,6 +39,14 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs return nullptr; } +// The catalog owns the borrowed preamble; each fixture owns its custom data mode. +class TupleTestHtMode : public Ieee80211HtMode +{ + public: + using Ieee80211HtMode::Ieee80211HtMode; + ~TupleTestHtMode() override { preambleMode = nullptr; } +}; + // Synthetic data rates isolate exact tuple matching from public lookup tolerance. class CustomBitrateDataMode : public Ieee80211HtDataMode { @@ -124,12 +132,14 @@ for (int mcsIndex = 0; mcsIndex < 32; mcsIndex++) { } } } -// No preamble is needed: these synthetic modes exercise only tuple lookup. +// Borrow a complete catalog preamble for the control-response tables. +const auto *basePreamble = check_and_cast( + findHtMode(htModeSet, 0, MHz(20), Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG))->getPreambleMode(); 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); +TupleTestHtMode source("source", basePreamble, new CustomBitrateDataMode(*baseData, Mbps(6)), Ieee80211HtMode::BAND_2_4GHZ); +TupleTestHtMode nearby("nearby", basePreamble, new CustomBitrateDataMode(*baseData, Mbps(6.04)), Ieee80211HtMode::BAND_2_4GHZ); +TupleTestHtMode equal("equal", basePreamble, 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); From d55a6fab0de0b04f52da9fbe48efeccc4aa35578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 15 Aug 2026 20:33:01 +0200 Subject: [PATCH 60/91] feat(ieee80211): add HT compressed Block Ack Implement one-TID compressed BlockAckReq and 64-bit compressed BlockAck exchanges across the QoS MAC path. Correct BAR and BA Control and Starting Sequence Control serialization, including the Basic BAR wire length, little-endian fields, fragment-number packing, and compressed frame lengths. Extend originator and recipient agreement handling, HCF dispatch, frame sequences, rate selection, protection timing, reordering, and acknowledgment processing. Preserve immediate versus delayed ADDBA policy and return the required all-zero compressed Block Ack when no matching recipient state exists. Keep selection default-off behind the documented assumePeerSupportsCompressedBlockAck model assumption. Reject fragmented and delayed-policy exchanges, maintain the recipient acknowledgment-window boundary, and preserve leading holes including sequence-number wraparound. Add byte-exact unit coverage for Basic and compressed variants, bitmap and wraparound behavior, capability and agreement gates, plus a deterministic HT runtime exchange test. --- .../ieee80211/mac/Ieee80211Frame.msg | 3 +- .../mac/Ieee80211MacHeaderSerializer.cc | 23 +- .../ieee80211/mac/blockack/BlockAckRecord.cc | 15 +- .../ieee80211/mac/blockack/BlockAckRecord.h | 5 +- .../OriginatorBlockAckAgreementHandler.cc | 9 + .../blockack/OriginatorBlockAckProcedure.cc | 13 +- .../blockack/RecipientBlockAckAgreement.cc | 2 +- .../mac/blockack/RecipientBlockAckAgreement.h | 7 + .../RecipientBlockAckAgreementHandler.cc | 15 +- .../RecipientBlockAckAgreementHandler.h | 2 +- .../blockack/RecipientBlockAckProcedure.cc | 29 +- .../blockackreordering/BlockAckReordering.cc | 2 +- .../mac/contract/IOriginatorQoSAckPolicy.h | 1 + .../IRecipientBlockAckAgreementHandler.h | 2 +- .../mac/contract/IRecipientQosAckPolicy.h | 3 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 22 +- .../framesequence/PrimitiveFrameSequences.cc | 10 +- .../mac/originator/OriginatorQosAckPolicy.cc | 31 +- .../mac/originator/OriginatorQosAckPolicy.h | 5 +- .../mac/originator/OriginatorQosAckPolicy.ned | 4 +- .../SingleProtectionMechanism.cc | 7 +- .../mac/rateselection/QosRateSelection.cc | 5 +- .../mac/recipient/RecipientQosAckPolicy.cc | 23 +- .../mac/recipient/RecipientQosAckPolicy.h | 7 +- .../mac/recipient/RecipientQosAckPolicy.ned | 4 +- .../recipient/RecipientQosMacDataService.cc | 10 +- .../Ieee80211CompressedBlockAckRuntime.test | 37 ++ tests/unit/Ieee80211CompressedBlockAck_1.test | 319 ++++++++++++++++++ 28 files changed, 554 insertions(+), 61 deletions(-) create mode 100644 tests/module/Ieee80211CompressedBlockAckRuntime.test create mode 100644 tests/unit/Ieee80211CompressedBlockAck_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg index e139deaf75d..f671d96f975 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg @@ -43,6 +43,7 @@ const b LENGTH_ADDBAREQ = LENGTH_MGMT + B(9); // mgmt length + action body lengt const b LENGTH_ADDBARESP = LENGTH_MGMT + B(9); // mgmt length + action body length const b LENGTH_DELBA = LENGTH_MGMT + B(6); // mgmt length + action body length const b LENGTH_BASIC_BLOCKACK = B(16 + 2 + (2 + 128) + 4); // header + ba control + ba information + fcs +const b LENGTH_COMPRESSED_BLOCKACK = B(16 + 2 + (2 + 8) + 4); // header + ba control + ba information + fcs const b DATAFRAME_HEADER_MINLENGTH = B(2 + 2 + 3 * 6 + 2); //bits without QoS, without Address4: 2 + 2 + 3*6(addresses) + 2 const b QOSCONTROL_PART_LENGTH = b(2 * 8); // QoS Control field length (bits) const short int MAX_NUM_FRAGMENTS = 16; @@ -455,7 +456,7 @@ class Ieee80211BasicBlockAck extends Ieee80211BlockAck // class Ieee80211CompressedBlockAck extends Ieee80211BlockAck { - // chunkLength TODO + chunkLength = LENGTH_COMPRESSED_BLOCKACK - B(4); multiTid = 0; compressedBitmap = 1; diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index 50b149538f1..218b9f79e3a 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -90,6 +90,24 @@ void readSequenceControl(MemoryInputStream& stream, int& fragmentNumber, ieee802 sequenceNumber = ieee80211::SequenceNumberCyclic((sequenceControl >> 4) & 0xFFF); } +uint16_t packBlockAckControl(bool ackPolicy, bool multiTid, bool compressedBitmap, uint16_t reserved, uint8_t tidInfo) +{ + return (ackPolicy ? 0x0001 : 0) | + (multiTid ? 0x0002 : 0) | + (compressedBitmap ? 0x0004 : 0) | + ((reserved & 0x1FF) << 3) | + ((tidInfo & 0xF) << 12); +} + +void unpackBlockAckControl(uint16_t control, bool& ackPolicy, bool& multiTid, bool& compressedBitmap, uint16_t& reserved, uint8_t& tidInfo) +{ + ackPolicy = (control & 0x0001) != 0; + multiTid = (control & 0x0002) != 0; + compressedBitmap = (control & 0x0004) != 0; + reserved = (control >> 3) & 0x1FF; + tidInfo = (control >> 12) & 0xF; +} + } // namespace namespace ieee80211 { @@ -679,8 +697,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre std::vector bytes; bytes.push_back(stream.readByte()); bytes.push_back(stream.readByte()); - BitVector *blockAckBitmap = new BitVector(bytes); - basicBlockAck->setBlockAckBitmap(i, *blockAckBitmap); + basicBlockAck->setBlockAckBitmap(i, BitVector(bytes)); } return basicBlockAck; } @@ -699,7 +716,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre for (size_t i = 0; i < 8; ++i) { bytes.push_back(stream.readByte()); } - compressedBlockAck->setBlockAckBitmap(*(new BitVector(bytes))); + compressedBlockAck->setBlockAckBitmap(BitVector(bytes)); return compressedBlockAck; } else { diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 919bd813a6f..7dce744fdec 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -12,9 +12,10 @@ namespace inet { namespace ieee80211 { -BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid) : +BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) : originatorAddress(originatorAddress), - tid(tid) + tid(tid), + startingSequenceNumber(startingSequenceNumber) { } @@ -42,6 +43,13 @@ bool BlockAckRecord::getAckState(SequenceNumberCyclic sequenceNumber, FragmentNu } } +bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) +{ + // IEEE Std 802.11-2024, 10.25.6.1: bits preceding the maintained + // receive-window range are one; missing MPDUs within the range are zero. + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || sequenceNumber < startingSequenceNumber; +} + void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) { auto it = acknowledgmentState.begin(); @@ -51,8 +59,9 @@ void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) else it++; } + if (startingSequenceNumber <= sequenceNumber) + startingSequenceNumber = sequenceNumber + 1; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h index 971be8a8af9..3eb8f9bec46 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -24,14 +24,16 @@ class INET_API BlockAckRecord protected: MacAddress originatorAddress = MacAddress::UNSPECIFIED_ADDRESS; Tid tid = -1; + SequenceNumberCyclic startingSequenceNumber; std::map acknowledgmentState; public: - BlockAckRecord(MacAddress originatorAddress, Tid tid); + BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber); virtual ~BlockAckRecord() {} void blockAckPolicyFrameReceived(const Ptr& header); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); + bool getCompressedAckState(SequenceNumberCyclic sequenceNumber); void removeAckStates(SequenceNumberCyclic sequenceNumber); MacAddress getOriginatorAddress() { return originatorAddress; } @@ -42,4 +44,3 @@ class INET_API BlockAckRecord } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 32764c96f8f..5f7a60b665b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -169,6 +169,14 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { + auto agreement = getAgreement(compressedBlockAck->getTransmitterAddress(), compressedBlockAck->getTidInfo()); + if (agreement) { + agreement->setStartingSequenceNumber(compressedBlockAck->getStartingSequenceNumber()); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } + } else throw cRuntimeError("Unsupported BlockAck"); } @@ -318,6 +326,7 @@ void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgree { agreement->setIsAddbaResponseReceived(true); agreement->setIsAMsduSupported(addbaResp->getAMsduSupported()); + agreement->setIsDelayedBlockAckPolicySupported(addbaResp->getBlockAckPolicy() == 0); agreement->setBufferSize(addbaResp->getBufferSize()); agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue()); agreement->calculateExpirationTime(); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc index d02e412b644..6a468e94e02 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc @@ -12,13 +12,11 @@ namespace ieee80211 { const Ptr OriginatorBlockAckProcedure::buildCompressedBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const { - throw cRuntimeError("Unsupported feature"); - // TODO implement - // auto blockAckReq = makeShared(); - // blockAckReq->setReceiverAddress(receiverAddress); - // blockAckReq->setStartingSequenceNumber(startingSequenceNumber); - // blockAckReq->setTidInfo(tid); - // return blockAckReq; + auto blockAckReq = makeShared(); + blockAckReq->setReceiverAddress(receiverAddress); + blockAckReq->setStartingSequenceNumber(startingSequenceNumber); + blockAckReq->setTidInfo(tid); + return blockAckReq; } const Ptr OriginatorBlockAckProcedure::buildBasicBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const @@ -32,4 +30,3 @@ const Ptr OriginatorBlockAckProcedure::buildBasicBlockAckR } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc index 1d3f447b9b4..bc724cc5c94 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -19,7 +19,7 @@ RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddr generationId(generationId) { calculateExpirationTime(); - blockAckRecord = new BlockAckRecord(originatorAddress, tid); + blockAckRecord = new BlockAckRecord(originatorAddress, tid, startingSequenceNumber); } void RecipientBlockAckAgreement::blockAckPolicyFrameReceived(const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 4002ef76b2a..9f4f54ebffc 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,6 +21,8 @@ class INET_API RecipientBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; + bool isAddbaResponseSent = false; + bool isDelayedBlockAckPolicySupported = false; simtime_t expirationTime = -1; // The agreement stays installed until the timeout DELBA is transmitted; // prevent that pending teardown from being re-armed by late activity. @@ -43,6 +45,11 @@ class INET_API RecipientBlockAckAgreement : public cObject if (!inactivityExpired) expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual bool getIsAddbaResponseSent() const { return isAddbaResponseSent; } + virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } + + virtual void addbaResposneSent() { isAddbaResponseSent = true; } + virtual void setIsDelayedBlockAckPolicySupported(bool isDelayedBlockAckPolicySupported) { this->isDelayedBlockAckPolicySupported = isDelayedBlockAckPolicySupported; } virtual simtime_t getExpirationTime() { return expirationTime; } virtual bool isInactivityExpired() const { return inactivityExpired; } virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 81d06d247f7..1d7ed7641c4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -49,11 +49,18 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) +void RecipientBlockAckAgreementHandler::blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) { - auto agreement = getActiveAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + Tid tid = -1; + if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = basicBlockAckReq->getTidInfo(); + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = compressedBlockAckReq->getTidInfo(); + else + throw cRuntimeError("Unsupported BlockAckReq"); + auto agreement = getActiveAgreement(tid, blockAckReq->getTransmitterAddress()); if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -199,6 +206,8 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAd } else blockAckAgreements[id] = agreement; + agreement->addbaResposneSent(); + agreement->setIsDelayedBlockAckPolicySupported(addbaResponse->getBlockAckPolicy() == 0); scheduleInactivityTimer(agreementHandlerCallback); } procedureCallback->processMgmtFrame(addbaResponsePacket, addbaResponse); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 8792f070d7d..b00f9cff6b1 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -49,7 +49,7 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index 2c94d1cac47..050b85759db 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -24,13 +24,24 @@ void RecipientBlockAckProcedure::processReceivedBlockAckReq(Packet *blockAckPack auto agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(basicBlockAckReq->getTidInfo(), basicBlockAckReq->getTransmitterAddress()); if (ackPolicy->isBlockAckNeeded(basicBlockAckReq, agreement)) { auto blockAck = buildBlockAck(basicBlockAckReq, agreement); - auto duration = ackPolicy->computeBasicBlockAckDurationField(blockAckPacketReq, basicBlockAckReq); + auto duration = ackPolicy->computeBlockAckDurationField(blockAckPacketReq, basicBlockAckReq); blockAck->setDurationField(duration); auto blockAckPacket = new Packet("BasicBlockAck", blockAck); EV_DEBUG << "Duration for " << blockAckPacket->getName() << " is set to " << duration << " s.\n"; callback->transmitControlResponseFrame(blockAckPacket, blockAck, blockAckPacketReq, basicBlockAckReq); } } + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { + auto agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(compressedBlockAckReq->getTidInfo(), compressedBlockAckReq->getTransmitterAddress()); + if (ackPolicy->isBlockAckNeeded(compressedBlockAckReq, agreement)) { + auto blockAck = buildBlockAck(compressedBlockAckReq, agreement); + auto duration = ackPolicy->computeBlockAckDurationField(blockAckPacketReq, compressedBlockAckReq); + blockAck->setDurationField(duration); + auto blockAckPacket = new Packet("CompressedBlockAck", blockAck); + EV_DEBUG << "Duration for " << blockAckPacket->getName() << " is set to " << duration << " s.\n"; + callback->transmitControlResponseFrame(blockAckPacket, blockAck, blockAckPacketReq, compressedBlockAckReq); + } + } else throw cRuntimeError("Unsupported BlockAckReq"); } @@ -65,6 +76,22 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr blockAck->setTidInfo(basicBlockAckReq->getTidInfo()); return blockAck; } + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { + auto blockAck = makeShared(); + auto startingSequenceNumber = compressedBlockAckReq->getStartingSequenceNumber(); + BitVector bitmap(std::vector(8, 0)); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.25.6.1 and 10.25.6.5: a non-HE + // compressed BA reports 64 consecutive, unfragmented MPDUs. + for (int i = 0; i < 64; i++) + bitmap.setBit(i, agreement->getBlockAckRecord()->getCompressedAckState(startingSequenceNumber + i)); + } + blockAck->setReceiverAddress(blockAckReq->getTransmitterAddress()); + blockAck->setStartingSequenceNumber(startingSequenceNumber); + blockAck->setTidInfo(compressedBlockAckReq->getTidInfo()); + blockAck->setBlockAckBitmap(bitmap); + return blockAck; + } else throw cRuntimeError("Unsupported Block Ack Request"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 35d2e86507c..2d365570a22 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -97,7 +97,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq tid = basicReq->getTidInfo(); startingSequenceNumber = basicReq->getStartingSequenceNumber(); } - else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { + else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { tid = compressedReq->getTidInfo(); startingSequenceNumber = compressedReq->getStartingSequenceNumber(); } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 4cfc2e63c24..0fac80fddff 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -28,6 +28,7 @@ class INET_API IOriginatorQoSAckPolicy virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const = 0; virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; + virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const = 0; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const = 0; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 6841c5b17b0..d10380b03e3 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -44,7 +44,7 @@ class INET_API IRecipientBlockAckAgreementHandler virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h index ccf47cc5610..b0525470502 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h @@ -25,11 +25,10 @@ class INET_API IRecipientQosAckPolicy virtual bool isBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const = 0; virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const = 0; - virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const = 0; + virtual simtime_t computeBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 98684e331f9..4b1cb39ff69 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -796,7 +796,7 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); - else if (auto blockAckRequest = dynamicPtrCast(header)) { + else if (auto blockAckRequest = dynamicPtrCast(header)) { if (recipientBlockAckAgreementHandler) recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckRequest, this); if (recipientBlockAckProcedure) @@ -1238,10 +1238,18 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { - EV_INFO << "BasicBlockAck has arrived" << std::endl; - if (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo()) == nullptr) { - EV_INFO << "Ignoring BasicBlockAck without an active Block Ack agreement.\n"; + else if (auto blockAck = dynamicPtrCast(header)) { + EV_INFO << blockAck->getClassName() << " has arrived" << std::endl; + Tid tid = -1; + MacAddress transmitterAddress = blockAck->getTransmitterAddress(); + if (auto basicBlockAck = dynamicPtrCast(blockAck)) + tid = basicBlockAck->getTidInfo(); + else if (auto compressedBlockAck = dynamicPtrCast(blockAck)) + tid = compressedBlockAck->getTidInfo(); + else + throw cRuntimeError("Unknown BlockAck frame"); + if (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(transmitterAddress, tid) == nullptr) { + EV_INFO << "Ignoring BlockAck without an active Block Ack agreement.\n"; return; } edcaf->getRecoveryProcedure()->blockAckFrameReceived(); @@ -1259,7 +1267,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetRecoveryProcedure()->ctsFrameReceived(); else if (header->getType() == ST_DATA_WITH_QOS) ; // void - else if (dynamicPtrCast(header)) + else if (dynamicPtrCast(header)) ; // void else throw cRuntimeError("Unknown control frame"); @@ -1355,7 +1363,7 @@ void Hcf::transmitControlResponseFrame(Packet *responsePacket, const Ptr(receivedHeader)) responseMode = rateSelection->computeResponseCtsFrameMode(receivedPacket, rtsFrame); - else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) + else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseBlockAckFrameMode(receivedPacket, blockAckReq); else if (auto dataOrMgmtHeader = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseAckFrameMode(receivedPacket, dataOrMgmtHeader); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 2980f92720c..69289dc51fb 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -396,8 +396,14 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); - auto blockAckReq = context->getQoSContext()->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); - auto blockAckPacket = new Packet("BasicBlockAckReq", blockAckReq); + auto agreementHandler = context->getQoSContext()->blockAckAgreementHandler; + auto agreement = agreementHandler == nullptr ? nullptr : agreementHandler->getAgreement(receiverAddr, tid); + auto outstandingFrames = context->getInProgressFrames()->getOutstandingFrames(); + bool useCompressedBlockAck = context->getQoSContext()->ackPolicy->isCompressedBlockAckReq(outstandingFrames, agreement); + auto blockAckReq = useCompressedBlockAck ? + context->getQoSContext()->blockAckProcedure->buildCompressedBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber) : + context->getQoSContext()->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); + auto blockAckPacket = new Packet(useCompressedBlockAck ? "CompressedBlockAckReq" : "BasicBlockAckReq", blockAckReq); blockAckPacket->insertAtBack(makeShared()); return new TransmitStep(blockAckPacket, context->getIfs(), true); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 7e6de21f2f3..a1c748d4451 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -23,6 +23,7 @@ void OriginatorQosAckPolicy::initialize(int stage) rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); maxBlockAckPolicyFrameLength = par("maxBlockAckPolicyFrameLength"); blockAckReqThreshold = par("blockAckReqThreshold"); + assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); blockAckTimeout = par("blockAckTimeout"); ackTimeout = par("ackTimeout"); } @@ -61,15 +62,29 @@ SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const return startingSequenceNumber; } -bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const +bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const { - // The Compressed Bitmap subfield of the BA Control field or BAR Control field shall be set to 1 in all - // BlockAck and BlockAckReq frames sent from one HT STA to another HT STA and shall be set to 0 otherwise. - return false; // non-HT STA -// for (auto frame : outstandingFrames) -// if (frame->getSequenceNumber() >= startingSequenceNumber && frame->getFragmentNumber() > 0) -// return false; -// return true; + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupportsCompressedBlockAck); +} + +bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +{ + // IEEE Std 802.11-2024, Table 11-8 and 10.25.6.1: use the compressed + // variant only for an established immediate HT Block Ack agreement. + // Peer HT capability is not represented by the baseline agreement contract; + // the parameter is an explicit assumption supplied by the configuration. + if (!assumePeerSupportsCompressedBlockAck || agreement == nullptr || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) + return false; + bool hasMatchingOutstandingFrame = false; + for (auto frame : outstandingFrames) { + auto header = dynamicPtrCast(frame->peekAtFront()); + if (header == nullptr || header->getReceiverAddress() != agreement->getReceiverAddr() || header->getTid() != agreement->getTid()) + continue; + hasMatchingOutstandingFrame = true; + if (header->getFragmentNumber() != 0 || header->getMoreFragments()) + return false; + } + return hasMatchingOutstandingFrame; } // FIXME diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index 94f48ee7979..64e2eb62664 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -22,6 +22,7 @@ class INET_API OriginatorQosAckPolicy : public ModeSetModuleBase, public IOrigin IQosRateSelection *rateSelection = nullptr; int maxBlockAckPolicyFrameLength = -1; int blockAckReqThreshold = -1; + bool assumePeerSupportsCompressedBlockAck = false; simtime_t blockAckTimeout = -1; simtime_t ackTimeout = -1; @@ -33,14 +34,14 @@ class INET_API OriginatorQosAckPolicy : public ModeSetModuleBase, public IOrigin virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const; virtual std::map, std::vector> getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const; virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; - virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const; - + static bool isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); public: virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const override; virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; + virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned index 953c8901008..0eaa3b68a88 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned @@ -22,9 +22,11 @@ simple OriginatorQosAckPolicy extends SimpleModule like IOriginatorQosAckPolicy int blockAckReqThreshold = default(5); int maxBlockAckPolicyFrameLength @unit(B) = default(1000B); + // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. + // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. + bool assumePeerSupportsCompressedBlockAck = default(false); double blockAckTimeout @unit(s) = default(-1s); double ackTimeout @unit(s) = default(-1s); @display("i=block/control"); } - diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc index 47009317dcb..7485427ca68 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc @@ -74,8 +74,12 @@ simtime_t SingleProtectionMechanism::computeBlockAckReqDurationField(Packet *pac simtime_t blockAckReqDurationPerId = blockAckFrameDuration + modeSet->getSifsTime(); return blockAckReqDurationPerId; } + else if (dynamicPtrCast(blockAckReq)) { + simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq)->getDuration(LENGTH_COMPRESSED_BLOCKACK); + return blockAckFrameDuration + modeSet->getSifsTime(); + } else - throw cRuntimeError("Compressed and Multi-Tid Block Ack Requests are not supported"); + throw cRuntimeError("Multi-Tid Block Ack Requests are not supported"); } // @@ -183,4 +187,3 @@ simtime_t SingleProtectionMechanism::computeDurationField(Packet *packet, const } /* 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 7cb25cebcea..5db9a82e873 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -166,7 +166,7 @@ const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *pack // const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq) { - if (!dynamicPtrCast(blockAckReq)) + if (!dynamicPtrCast(blockAckReq) && !dynamicPtrCast(blockAckReq)) throw cRuntimeError("Unknown BlockAckReq frame type"); const IIeee80211Mode *responseMode = nullptr; if (responseBlockAckFrameMode) @@ -178,7 +178,8 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet // this model has no BSSBasicRateSet/HT Control context to select another PPDU. responseMode = modeSet->getNonHtControlResponseMode(mode, false); } - return blockAckReq ? getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), responseMode) : responseMode; + auto peerAddress = mib && blockAckReq->getReceiverAddress() == mib->address ? blockAckReq->getTransmitterAddress() : blockAckReq->getReceiverAddress(); + return getPeerCompatibleMode(peerAddress, responseMode); } const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 09e1d5ea710..f5075d314b6 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -20,12 +20,14 @@ void RecipientQosAckPolicy::initialize(int stage) ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); + assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); } } -simtime_t RecipientQosAckPolicy::computeBasicBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const +simtime_t RecipientQosAckPolicy::computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const { - return rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq)->getDuration(LENGTH_BASIC_BLOCKACK); + b length = dynamicPtrCast(blockAckReq) ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK; + return rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq)->getDuration(length); } simtime_t RecipientQosAckPolicy::computeAckDuration(Packet *packet, const Ptr& dataOrMgmtHeader) const @@ -67,10 +69,23 @@ bool RecipientQosAckPolicy::isBlockAckNeeded(const Ptr(blockAckReq)) + return isCompressedBlockAckNeeded(compressedBlockAckReq, agreement, assumePeerSupportsCompressedBlockAck); else throw cRuntimeError("Unsupported BlockAckReq"); } +bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +{ + // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5. Both endpoint policies + // must carry the explicit peer-capability assumption because this baseline + // does not model negotiated peer HT capability. + if (!assumePeerSupportsCompressedBlockAck || blockAckReq->getFragmentNumber() != 0) + return false; + // A missing partial state still elicits the mandatory null compressed BA. + return agreement == nullptr || (agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported()); +} + // // 8.2.5.7 Setting for control response frames // For an ACK frame, the Duration/ID field is set to the value obtained from the Duration/ID field of the frame @@ -90,9 +105,9 @@ simtime_t RecipientQosAckPolicy::computeAckDurationField(Packet *packet, const P // the PPDU carrying the frame that elicited the response and the end of the PPDU carrying the BlockAck // frame. // -simtime_t RecipientQosAckPolicy::computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const +simtime_t RecipientQosAckPolicy::computeBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const { - return basicBlockAckReq->getDurationField() - modeSet->getSifsTime() - computeBasicBlockAckDuration(packet, basicBlockAckReq); + return blockAckReq->getDurationField() - modeSet->getSifsTime() - computeBlockAckDuration(packet, blockAckReq); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index 931635fc406..9e3de32b03c 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -20,12 +20,14 @@ class INET_API RecipientQosAckPolicy : public ModeSetModuleBase, public IRecipie { protected: IQosRateSelection *rateSelection = nullptr; + bool assumePeerSupportsCompressedBlockAck = false; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - simtime_t computeBasicBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const; + static bool isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); + simtime_t computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const; simtime_t computeAckDuration(Packet *packet, const Ptr& dataOrMgmtHeader) const; public: @@ -33,11 +35,10 @@ class INET_API RecipientQosAckPolicy : public ModeSetModuleBase, public IRecipie virtual bool isBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const override; virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const override; - virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const override; + virtual simtime_t computeBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const override; }; } /* namespace ieee80211 */ } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned index ec3ceb199de..f4a5f896c51 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned @@ -19,6 +19,8 @@ simple RecipientQosAckPolicy extends SimpleModule like IRecipientQosAckPolicy string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(RecipientQosAckPolicy); string rateSelectionModule; + // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. + // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. + bool assumePeerSupportsCompressedBlockAck = default(false); @display("i=block/control"); } - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index bb69050ed7e..b1222c6e72b 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -249,10 +249,16 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c { Enter_Method("controlFrameReceived"); expireReceiveLifetime(); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReorderBuffer frames; if (blockAckReordering) { - Tid tid = blockAckReq->getTidInfo(); + Tid tid = -1; + if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = basicBlockAckReq->getTidInfo(); + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = compressedBlockAckReq->getTidInfo(); + else + return std::vector(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test new file mode 100644 index 00000000000..ea7c4dbdc24 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -0,0 +1,37 @@ +%description: + +Checks that an explicitly enabled one-TID HT-immediate Block Ack agreement uses +Compressed BlockAckReq and receives the SIFS Compressed BlockAck response. + +%extraargs: -c CompressedBlockAckRuntime -r 0 + +%inifile: omnetpp.ini + +include ../../../../examples/wireless/qos/omnetpp.ini + +[General] +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +seed-set = 0 + +[Config CompressedBlockAckRuntime] +extends = MacQosWithBlockAck +abstract = false +sim-time-limit = 1.05s + +**.opMode = "n(mixed-2.4Ghz)" +# Baseline capability management is absent, so both endpoint policies explicitly +# opt in to compressed Block Ack; the production default remains false. +**.assumePeerSupportsCompressedBlockAck = true +**.cmdenv-log-level = info + +%contains: stdout +Processing Addba Request from + +%contains: stdout +Processing transmitted frame CompressedBlockAckReq as originator in frame sequence. + +%contains: stdout +Ieee80211CompressedBlockAck has arrived diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test new file mode 100644 index 00000000000..eeb7f6fb503 --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -0,0 +1,319 @@ +%description: +Validate one-TID HT Compressed Block Ack wire encoding and recipient bitmap construction. +IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. + +%includes: +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure +{ + public: + using RecipientBlockAckProcedure::buildBlockAck; +}; + +class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler +{ + public: + using OriginatorBlockAckAgreementHandler::updateAgreement; +}; + +class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy +{ + public: + static bool isCompressedRequestNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupport) + { + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupport); + } +}; + +class TestRecipientQosAckPolicy : public RecipientQosAckPolicy +{ + public: + static bool isCompressedResponseNeeded(const Ptr& request, RecipientBlockAckAgreement *agreement, bool enabled) + { + return isCompressedBlockAckNeeded(request, agreement, enabled); + } +}; + +template +Ptr roundTrip(const Ptr& original) +{ + Packet packet("original", original); + auto bytes = packet.peekAllAsBytes(); + Packet decodedPacket("decoded", bytes); + auto decoded = decodedPacket.popAtFront(); + return dynamicPtrCast(decoded); +} + +static Ptr makeCompressedBlockAckReq() +{ + auto request = makeShared(); + request->setDurationField(SIMTIME_ZERO); + request->setReceiverAddress(MacAddress("10:20:30:40:50:60")); + request->setTransmitterAddress(MacAddress("11:22:33:44:55:66")); + request->setBarAckPolicy(true); + request->setTidInfo(5); + request->setFragmentNumber(0); + request->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + return request; +} + +static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, FragmentNumber fragmentNumber = 0, bool moreFragments = false) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiverAddress); + header->setTid(tid); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + return new Packet("outstanding", header); +} + +%activity: +{ + auto request = makeCompressedBlockAckReq(); + Packet packet("compressedBar", request); + auto bytes = packet.peekAllAsBytes(); + ASSERT(bytes->getChunkLength() == B(20)); + ASSERT(bytes->getByte(16) == 0x05 && bytes->getByte(17) == 0x50); // BAR Control: policy=1, compressed=1, TID=5 + ASSERT(bytes->getByte(18) == 0xC0 && bytes->getByte(19) == 0xAB); // SSC: fragment=0, sequence=0xABC + auto decoded = roundTrip(request); + ASSERT(decoded != nullptr); + ASSERT(decoded->getBarAckPolicy() && decoded->getCompressedBitmap() && !decoded->getMultiTid()); + ASSERT(decoded->getTidInfo() == 5 && decoded->getFragmentNumber() == 0); + ASSERT(decoded->getStartingSequenceNumber() == SequenceNumberCyclic(0xABC)); + EV << "Compressed BAR encoding and round-trip passed.\n"; +} + +{ + auto blockAck = makeShared(); + blockAck->setDurationField(SIMTIME_ZERO); + blockAck->setReceiverAddress(MacAddress("11:22:33:44:55:66")); + blockAck->setTransmitterAddress(MacAddress("10:20:30:40:50:60")); + blockAck->setTidInfo(5); + blockAck->setFragmentNumber(0); + blockAck->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + BitVector bitmap(std::vector({0x05, 0, 0, 0, 0, 0, 0, 0})); + blockAck->setBlockAckBitmap(bitmap); + Packet packet("compressedBa", blockAck); + auto bytes = packet.peekAllAsBytes(); + ASSERT(bytes->getChunkLength() == B(28)); + ASSERT(bytes->getByte(16) == 0x04 && bytes->getByte(17) == 0x50); // BA Control: compressed=1, TID=5 + ASSERT(bytes->getByte(18) == 0xC0 && bytes->getByte(19) == 0xAB); + ASSERT(bytes->getByte(20) == 0x05); + auto decoded = roundTrip(blockAck); + ASSERT(decoded != nullptr); + ASSERT(decoded->getBlockAckBitmap().getBit(0)); + ASSERT(!decoded->getBlockAckBitmap().getBit(1)); + ASSERT(decoded->getBlockAckBitmap().getBit(2)); + EV << "Compressed BA encoding and round-trip passed.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, nullptr)); + ASSERT(response != nullptr && response->getChunkLength() == B(28)); + for (int i = 0; i < 64; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + EV << "Missing recipient state produces a null compressed BA.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement emptyAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &emptyAgreement)); + for (int i = 0; i < 64; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + EV << "Empty recipient record produces a null compressed BA.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(101), 64, SIMTIME_ZERO); + for (int sequenceNumber : {101, 103}) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + } + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(101)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(1)); + ASSERT(response->getBlockAckBitmap().getBit(2)); + EV << "Established agreement produces the expected 64-bit bitmap.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(102)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(!response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(1)); + ASSERT(response->getBlockAckBitmap().getBit(2)); + EV << "Compressed bitmap preserves leading receive-window holes.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4095), 64, SIMTIME_ZERO); + for (int sequenceNumber : {4095, 0}) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + } + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(response->getBlockAckBitmap().getBit(0)); + ASSERT(response->getBlockAckBitmap().getBit(1)); + ASSERT(!response->getBlockAckBitmap().getBit(2)); + EV << "Compressed bitmap wraps from sequence 4095 to 0.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4095), 64, SIMTIME_ZERO); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(1)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(!response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(1)); + ASSERT(response->getBlockAckBitmap().getBit(2)); + EV << "Compressed bitmap preserves leading holes across sequence wrap.\n"; +} + +{ + auto request = makeCompressedBlockAckReq(); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, false)); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + request->setFragmentNumber(1); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + request->setFragmentNumber(0); + RecipientBlockAckAgreement delayedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + delayedAgreement.addbaResposneSent(); + delayedAgreement.setIsDelayedBlockAckPolicySupported(true); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); + delayedAgreement.setIsDelayedBlockAckPolicySupported(false); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); + EV << "Recipient capability, fragment, and agreement-policy gates passed.\n"; +} + +{ + MacAddress receiverAddress("10:20:30:40:50:60"); + OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + immediateAgreement.setIsAddbaResponseReceived(true); + std::vector outstandingFrames { makeOutstandingQosFrame(receiverAddress, 5) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, false)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr, true)); + OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true); + delayedAgreement.setIsAddbaResponseReceived(true); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement, true)); + ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delete outstandingFrames[0]; + outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 1) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delete outstandingFrames[0]; + outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 0, true) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delete outstandingFrames[0]; + EV << "Originator capability, agreement, and fragmentation gates passed.\n"; +} + +{ + auto basicRequest = makeShared(); + basicRequest->setDurationField(SIMTIME_ZERO); + basicRequest->setReceiverAddress(MacAddress("10:20:30:40:50:60")); + basicRequest->setTransmitterAddress(MacAddress("11:22:33:44:55:66")); + basicRequest->setTidInfo(5); + basicRequest->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + Packet packet("basicBar", basicRequest); + auto bytes = packet.peekAllAsBytes(); + ASSERT(bytes->getChunkLength() == B(20)); + ASSERT(bytes->getByte(16) == 0x00 && bytes->getByte(17) == 0x50); + ASSERT(roundTrip(basicRequest) != nullptr); + auto basicBlockAck = makeShared(); + basicBlockAck->setDurationField(SIMTIME_ZERO); + basicBlockAck->setReceiverAddress(MacAddress("11:22:33:44:55:66")); + basicBlockAck->setTransmitterAddress(MacAddress("10:20:30:40:50:60")); + basicBlockAck->setTidInfo(5); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + for (int i = 0; i < 64; i++) + basicBlockAck->setBlockAckBitmap(i, BitVector(std::vector(2, 0))); + Packet blockAckPacket("basicBa", basicBlockAck); + auto blockAckBytes = blockAckPacket.peekAllAsBytes(); + ASSERT(blockAckBytes->getChunkLength() == B(148)); + ASSERT(blockAckBytes->getByte(16) == 0x00 && blockAckBytes->getByte(17) == 0x50); + ASSERT(blockAckBytes->getByte(18) == 0xC0 && blockAckBytes->getByte(19) == 0xAB); + ASSERT(roundTrip(basicBlockAck) != nullptr); + EV << "Legacy Basic BAR and BA remain byte-exact.\n"; +} + +{ + TestOriginatorBlockAckAgreementHandler handler; + OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false); + auto delayedResponse = makeShared(); + delayedResponse->setBlockAckPolicy(false); + delayedResponse->setBufferSize(64); + delayedResponse->setBlockAckTimeoutValue(SIMTIME_ZERO); + handler.updateAgreement(&agreement, delayedResponse); + ASSERT(agreement.getIsAddbaResponseReceived()); + ASSERT(agreement.getIsDelayedBlockAckPolicySupported()); + EV << "Accepted delayed agreement is retained for Basic BAR fallback.\n"; +} + +EV << ".\n"; + +%contains: stdout +Compressed BAR encoding and round-trip passed. +Compressed BA encoding and round-trip passed. +Missing recipient state produces a null compressed BA. +Empty recipient record produces a null compressed BA. +Established agreement produces the expected 64-bit bitmap. +Compressed bitmap preserves leading receive-window holes. +Compressed bitmap wraps from sequence 4095 to 0. +Compressed bitmap preserves leading holes across sequence wrap. +Recipient capability, fragment, and agreement-policy gates passed. +Originator capability, agreement, and fragmentation gates passed. +Legacy Basic BAR and BA remain byte-exact. +Accepted delayed agreement is retained for Basic BAR fallback. +. From fccae3ea1b03df71c1c78f381bdeb7723bfc1b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 15 Aug 2026 21:51:34 +0200 Subject: [PATCH 61/91] fix(ieee80211): select Block Ack rates by base variant Classify BlockAck and BlockAckReq frames through their base classes in QosRateSelection so compressed requests follow the same control-rate policy as Basic requests. This prevents an originated Compressed BlockAckReq from falling through to the ordinary control-frame path and reusing the last transmitted data mode when the mandatory Block Ack fallback should be selected. Add focused coverage with distinct mandatory and last-transmitted modes, verifying Basic and Compressed BAR symmetry while preserving RTS behavior. --- .../mac/rateselection/QosRateSelection.cc | 9 ++--- tests/unit/Ieee80211CompressedBlockAck_1.test | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 5db9a82e873..20d95b3e58d 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -213,17 +213,18 @@ const IIeee80211Mode *QosRateSelection::computeControlFrameMode(const Ptr(header) != nullptr || dynamicPtrCast(header) != nullptr; // This subclause describes the rate selection rules for control frames that initiate a TXOP and that are not carried // in an A-MPDU. if (txopProcedure->isTxopInitiator(header)) { - // If a control frame other than a Basic BlockAckReq or Basic BlockAck is carried in a non-HT PPDU, the + // If a control frame other than a BlockAckReq or BlockAck is carried in a non-HT PPDU, the // transmitting STA shall transmit the frame using one of the rates in the BSSBasicRateSet parameter or a rate // from the mandatory rate set of the attached PHY if the BSSBasicRateSet is empty. - if (!dynamicPtrCast(header) && !dynamicPtrCast(header)) { + if (!isBlockAckFrame) { // TODO BSSBasicRateSet return fastestMandatoryMode; } - // If a Basic BlockAckReq or Basic BlockAck frame is carried in a non-HT PPDU, the transmitting STA shall + // If a BlockAckReq or BlockAck frame is carried in a non-HT PPDU, the transmitting STA shall // transmit the frame using a rate supported by the receiver STA, if known (as reported in the Supported Rates // element and/or Extended Supported Rates element in frames transmitted by that STA). If the supported rate set // of the receiving STA or STAs is not known, the transmitting STA shall transmit using a rate from the @@ -245,7 +246,7 @@ const IIeee80211Mode *QosRateSelection::computeControlFrameMode(const Ptr(header) && !dynamicPtrCast(header)) { + if (!isBlockAckFrame) { // TODO frame sequence context auto it = lastTransmittedFrameMode.find(header->getReceiverAddress()); return (it != lastTransmittedFrameMode.end()) ? it->second : fastestMandatoryMode; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index eeb7f6fb503..2fd6b9732b5 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -10,7 +10,10 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" %global: using namespace inet; @@ -46,6 +49,14 @@ class TestRecipientQosAckPolicy : public RecipientQosAckPolicy } }; +class TestQosRateSelection : public QosRateSelection +{ + public: + void setFastestMandatoryMode(const physicallayer::IIeee80211Mode *mode) { fastestMandatoryMode = mode; } + void setLastTransmittedFrameMode(const MacAddress& receiverAddress, const physicallayer::IIeee80211Mode *mode) { lastTransmittedFrameMode[receiverAddress] = mode; } + using QosRateSelection::computeControlFrameMode; +}; + template Ptr roundTrip(const Ptr& original) { @@ -301,6 +312,29 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag EV << "Accepted delayed agreement is retained for Basic BAR fallback.\n"; } +{ + // IEEE Std 802.11-2024, 10.6.6.4 applies the control-frame rate rule to BlockAckReq and BlockAck variants. + const auto mandatoryMode = &physicallayer::Ieee80211OfdmCompliantModes::ofdmMode6MbpsCS20MHz; + const auto lastTransmittedMode = &physicallayer::Ieee80211OfdmCompliantModes::ofdmMode54Mbps; + MacAddress receiverAddress("10:20:30:40:50:60"); + TestQosRateSelection rateSelection; + TxopProcedure txopProcedure; + rateSelection.setFastestMandatoryMode(mandatoryMode); + rateSelection.setLastTransmittedFrameMode(receiverAddress, lastTransmittedMode); + + auto basicRequest = makeShared(); + basicRequest->setReceiverAddress(receiverAddress); + auto compressedRequest = makeCompressedBlockAckReq(); + compressedRequest->setReceiverAddress(receiverAddress); + ASSERT(rateSelection.computeControlFrameMode(basicRequest, &txopProcedure) == mandatoryMode); + ASSERT(rateSelection.computeControlFrameMode(compressedRequest, &txopProcedure) == mandatoryMode); + + auto rtsFrame = makeShared(); + rtsFrame->setReceiverAddress(receiverAddress); + ASSERT(rateSelection.computeControlFrameMode(rtsFrame, &txopProcedure) == lastTransmittedMode); + EV << "Basic and Compressed BAR rate selection is variant-agnostic.\n"; +} + EV << ".\n"; %contains: stdout @@ -316,4 +350,5 @@ Recipient capability, fragment, and agreement-policy gates passed. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. +Basic and Compressed BAR rate selection is variant-agnostic. . From 5992b7d8497ac47ff2d39ac732e6227222987e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 01:25:44 +0200 Subject: [PATCH 62/91] fix(ieee80211): answer valid compressed block ack requests Remove the recipient-side peer capability assumption from compressed Block Ack response selection. An addressed, syntactically valid one-TID compressed BlockAckReq must receive a compressed BlockAck after SIFS, including the all-zero response when no matching partial state exists, as required by IEEE 802.11-2024 sections 9.3.1.7.2 and 10.25.6.5. Keep the originator-side capability assumption as the explicit opt-in for selecting compressed BlockAckReq frames until per-peer HT capability management is modeled. Continue rejecting nonzero fragment numbers and suppressing immediate responses for unaccepted or delayed agreements. Update the focused unit and runtime coverage to verify null-state responses, malformed fragment rejection, agreement-policy gates, and asymmetric endpoint configuration. --- .../mac/recipient/RecipientQosAckPolicy.cc | 15 ++++++--------- .../mac/recipient/RecipientQosAckPolicy.h | 3 +-- .../mac/recipient/RecipientQosAckPolicy.ned | 3 --- .../Ieee80211CompressedBlockAckRuntime.test | 6 +++--- tests/unit/Ieee80211CompressedBlockAck_1.test | 18 +++++++++--------- 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index f5075d314b6..b8ccc55eb88 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -18,10 +18,8 @@ Define_Module(RecipientQosAckPolicy); void RecipientQosAckPolicy::initialize(int stage) { ModeSetModuleBase::initialize(stage); - if (stage == INITSTAGE_LOCAL) { + if (stage == INITSTAGE_LOCAL) rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); - assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); - } } simtime_t RecipientQosAckPolicy::computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const @@ -70,17 +68,16 @@ bool RecipientQosAckPolicy::isBlockAckNeeded(const Ptr(blockAckReq)) - return isCompressedBlockAckNeeded(compressedBlockAckReq, agreement, assumePeerSupportsCompressedBlockAck); + return isCompressedBlockAckNeeded(compressedBlockAckReq, agreement); else throw cRuntimeError("Unsupported BlockAckReq"); } -bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) { - // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5. Both endpoint policies - // must carry the explicit peer-capability assumption because this baseline - // does not model negotiated peer HT capability. - if (!assumePeerSupportsCompressedBlockAck || blockAckReq->getFragmentNumber() != 0) + // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5: an addressed, syntactically + // valid Compressed BlockAckReq elicits a Compressed BlockAck, including a null response. + if (blockAckReq->getFragmentNumber() != 0) return false; // A missing partial state still elicits the mandatory null compressed BA. return agreement == nullptr || (agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported()); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index 9e3de32b03c..d47b88d6fba 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -20,13 +20,12 @@ class INET_API RecipientQosAckPolicy : public ModeSetModuleBase, public IRecipie { protected: IQosRateSelection *rateSelection = nullptr; - bool assumePeerSupportsCompressedBlockAck = false; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - static bool isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); + static bool isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement); simtime_t computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const; simtime_t computeAckDuration(Packet *packet, const Ptr& dataOrMgmtHeader) const; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned index f4a5f896c51..89eea501c93 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned @@ -19,8 +19,5 @@ simple RecipientQosAckPolicy extends SimpleModule like IRecipientQosAckPolicy string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC // Configured MAC catalog provider @class(RecipientQosAckPolicy); string rateSelectionModule; - // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. - // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. - bool assumePeerSupportsCompressedBlockAck = default(false); @display("i=block/control"); } diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test index ea7c4dbdc24..8dde5158a5c 100644 --- a/tests/module/Ieee80211CompressedBlockAckRuntime.test +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -22,9 +22,9 @@ abstract = false sim-time-limit = 1.05s **.opMode = "n(mixed-2.4Ghz)" -# Baseline capability management is absent, so both endpoint policies explicitly -# opt in to compressed Block Ack; the production default remains false. -**.assumePeerSupportsCompressedBlockAck = true +# The originator explicitly opts in based on its peer-capability assumption; +# the recipient requires no additional station-wide capability setting. +**.originatorAckPolicy.assumePeerSupportsCompressedBlockAck = true **.cmdenv-log-level = info %contains: stdout diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 2fd6b9732b5..8d34b512d95 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -43,9 +43,9 @@ class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy class TestRecipientQosAckPolicy : public RecipientQosAckPolicy { public: - static bool isCompressedResponseNeeded(const Ptr& request, RecipientBlockAckAgreement *agreement, bool enabled) + static bool isCompressedResponseNeeded(const Ptr& request, RecipientBlockAckAgreement *agreement) { - return isCompressedBlockAckNeeded(request, agreement, enabled); + return isCompressedBlockAckNeeded(request, agreement); } }; @@ -235,18 +235,18 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { auto request = makeCompressedBlockAckReq(); - ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, false)); - ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(1); - ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(0); RecipientBlockAckAgreement delayedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.addbaResposneSent(); delayedAgreement.setIsDelayedBlockAckPolicySupported(true); - ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.setIsDelayedBlockAckPolicySupported(false); - ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); - EV << "Recipient capability, fragment, and agreement-policy gates passed.\n"; + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); + EV << "Recipient fragment and agreement-policy gates passed.\n"; } { @@ -346,7 +346,7 @@ Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. Compressed bitmap wraps from sequence 4095 to 0. Compressed bitmap preserves leading holes across sequence wrap. -Recipient capability, fragment, and agreement-policy gates passed. +Recipient fragment and agreement-policy gates passed. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. From f963a3a46dc455b386c9a913dd5a205b28d733f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 11:18:20 +0200 Subject: [PATCH 63/91] fix(ieee80211): correct Block Ack receive-window handling Make BlockAckRecord the authoritative owner of WinStartR and update the scoreboard for every successfully received related Data MPDU. Advance it from newer Data frames and BlockAckReq starting sequence numbers instead of coupling acknowledgment state to upward packet delivery. Fix empty Basic Block Ack records so current-window sequence and fragment entries are reported as unacknowledged while entries older than WinStartR remain acknowledged. Keep the reordering window independent from the acknowledgment window. When a future MPDU or BAR advances the window, deliver complete displaced MSDUs, discard incomplete stale entries, retain packets beyond gaps, and preserve cyclic delivery order across the 4095-to-0 boundary. Detach returned packets before deleting stale receive-buffer state. Replace the station-wide compressed Block Ack assumption with capability state stored per agreement. Derive it from explicit local support and a configured peer-address list until HT Capabilities elements are modeled. Use compressed BAR only for established immediate agreements that support it, and suppress compressed Block Ack responses when no agreement exists. Add focused unit and runtime coverage for Basic and Compressed bitmaps, Data- and BAR-driven window movement, wraparound, gaps, fragments, duplicates, ownership, Normal Ack reception, capability gating, null responses, serialization, and the BAR-to-BA exchange. Multi-TID Block Ack remains unsupported. BA Control bit 0 remains clear because it is reserved by IEEE 802.11-2024, rather than a BA Ack Policy bit. Fingerprint validation found five expected maintained-ingredient changes in QoS and Block Ack scenarios. The three previously retained tyf values were also rechecked and found stale. Fingerprint CSV updates are intentionally not included pending separate approval. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 28 +- .../ieee80211/mac/blockack/BlockAckRecord.h | 5 +- .../blockack/OriginatorBlockAckAgreement.h | 3 + .../OriginatorBlockAckAgreementHandler.cc | 10 +- .../OriginatorBlockAckAgreementHandler.h | 4 +- .../OriginatorBlockAckAgreementPolicy.cc | 3 + .../OriginatorBlockAckAgreementPolicy.h | 3 + .../OriginatorBlockAckAgreementPolicy.ned | 2 + .../blockack/RecipientBlockAckAgreement.cc | 8 +- .../mac/blockack/RecipientBlockAckAgreement.h | 5 +- .../blockackreordering/BlockAckReordering.cc | 86 ++-- .../blockackreordering/BlockAckReordering.h | 6 +- .../IOriginatorBlockAckAgreementPolicy.h | 1 + .../mac/originator/OriginatorQosAckPolicy.cc | 10 +- .../mac/originator/OriginatorQosAckPolicy.h | 3 +- .../mac/originator/OriginatorQosAckPolicy.ned | 3 - .../mac/recipient/RecipientQosAckPolicy.cc | 7 +- .../recipient/RecipientQosMacDataService.cc | 2 +- .../Ieee80211CompressedBlockAckRuntime.test | 9 +- tests/unit/Ieee80211AddbaTransaction_1.test | 9 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 386 +++++++++++++++++- 21 files changed, 493 insertions(+), 100 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 7dce744fdec..14a21493581 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -19,10 +19,14 @@ BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNu { } -void BlockAckRecord::blockAckPolicyFrameReceived(const Ptr& header) +void BlockAckRecord::dataFrameReceived(const Ptr& header, int windowSize) { SequenceNumberCyclic sequenceNumber = header->getSequenceNumber(); FragmentNumber fragmentNumber = header->getFragmentNumber(); + // IEEE Std 802.11-2024, 10.25.6.3(b) and 10.25.6.4(c): a related + // MPDU beyond WinEndR advances the receive window before its bit is set. + if (startingSequenceNumber + windowSize <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048) + advanceStartingSequenceNumber(sequenceNumber - windowSize + 1); acknowledgmentState[SequenceControlField(sequenceNumber.get(), fragmentNumber)] = true; } @@ -31,16 +35,7 @@ bool BlockAckRecord::getAckState(SequenceNumberCyclic sequenceNumber, FragmentNu // The status of MPDUs that are considered “old” and prior to the sequence number // range for which the receiver maintains status shall be reported as successfully // received (i.e., the corresponding bit in the bitmap shall be set to 1). - if (containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber))) { - return true; - } - else if (acknowledgmentState.size() == 0) { - return true; // TODO old? - } - else { - auto earliest = acknowledgmentState.begin(); - return SequenceNumberCyclic(earliest->first.getSequenceNumber()) > sequenceNumber; // old = true - } + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber)) || sequenceNumber < startingSequenceNumber; } bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) @@ -50,17 +45,20 @@ bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || sequenceNumber < startingSequenceNumber; } -void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) +void BlockAckRecord::advanceStartingSequenceNumber(SequenceNumberCyclic newStartingSequenceNumber) { + // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: advance WinStartR + // for a newer related MPDU or BAR SSN, using the 12-bit sequence space. + if (!(startingSequenceNumber < newStartingSequenceNumber)) + return; auto it = acknowledgmentState.begin(); while (it != acknowledgmentState.end()) { - if (SequenceNumberCyclic(it->first.getSequenceNumber()) < sequenceNumber) + if (SequenceNumberCyclic(it->first.getSequenceNumber()) < newStartingSequenceNumber) it = acknowledgmentState.erase(it); else it++; } - if (startingSequenceNumber <= sequenceNumber) - startingSequenceNumber = sequenceNumber + 1; + startingSequenceNumber = newStartingSequenceNumber; } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h index 3eb8f9bec46..c4bb3783ceb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -31,13 +31,14 @@ class INET_API BlockAckRecord BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber); virtual ~BlockAckRecord() {} - void blockAckPolicyFrameReceived(const Ptr& header); + void dataFrameReceived(const Ptr& header, int windowSize); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); bool getCompressedAckState(SequenceNumberCyclic sequenceNumber); - void removeAckStates(SequenceNumberCyclic sequenceNumber); + void advanceStartingSequenceNumber(SequenceNumberCyclic startingSequenceNumber); MacAddress getOriginatorAddress() { return originatorAddress; } Tid getTid() { return tid; } + SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index d4d1644a16b..28ae7fb2f9b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -31,6 +31,7 @@ class INET_API OriginatorBlockAckAgreement : public cObject int bufferSize = -1; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; + bool isCompressedBlockAckSupported = false; OriginatorBlockAckAgreementState state = OriginatorBlockAckAgreementState::PENDING; bool isAddbaRequestSent = false; uint8_t dialogToken = 0; @@ -65,6 +66,7 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; } virtual bool getIsAMsduSupported() const { return isAMsduSupported; } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } + virtual bool getIsCompressedBlockAckSupported() const { return isCompressedBlockAckSupported; } virtual MacAddress getReceiverAddr() const { return receiverAddr; } virtual Tid getTid() const { return tid; } virtual const simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } @@ -78,6 +80,7 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual void setIsAddbaRequestSent(bool isAddbaRequestSent) { this->isAddbaRequestSent = isAddbaRequestSent; } virtual void setIsAMsduSupported(bool isAMsduSupported) { this->isAMsduSupported = isAMsduSupported; } virtual void setIsDelayedBlockAckPolicySupported(bool isDelayedBlockAckPolicySupported) { this->isDelayedBlockAckPolicySupported = isDelayedBlockAckPolicySupported; } + virtual void setIsCompressedBlockAckSupported(bool isCompressedBlockAckSupported) { this->isCompressedBlockAckSupported = isCompressedBlockAckSupported; } virtual void setBlockAckTimeoutValue(const simtime_t blockAckTimeoutValue) { this->blockAckTimeoutValue = blockAckTimeoutValue; } virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 5f7a60b665b..385ca9ee757 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -18,10 +18,11 @@ namespace inet { namespace ieee80211 { -void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest, uint64_t transactionId) +void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest, uint64_t transactionId, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) { ASSERT(addbaRequest->getDialogToken() != 0); OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0, addbaRequest->getDialogToken(), transactionId); + blockAckAgreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaRequest->getReceiverAddress())); auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid()); blockAckAgreements[agreementId] = blockAckAgreement; } @@ -270,7 +271,7 @@ uint64_t OriginatorBlockAckAgreementHandler::processAcknowledgedDataFrame(Packet // after the acknowledged trigger MPDU. auto addbaReq = buildAddbaRequest(receiverAddr, tid, dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); auto transactionId = nextTransactionId++; - createAgreement(addbaReq, transactionId); + createAgreement(addbaReq, transactionId, blockAckAgreementPolicy); auto addbaPacket = new Packet("AddbaReq", addbaReq); addbaPacket->addTag()->setTransactionId(transactionId); procedureCallback->processMgmtFrame(addbaPacket, addbaReq); @@ -289,7 +290,7 @@ OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processR bool acceptedByLocalPolicy = addbaResp->getStatusCode() == 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement); if (addbaResp->getStatusCode() == 0) { auto transactionId = agreement->getTransactionId(); - updateAgreement(agreement, addbaResp); + updateAgreement(agreement, addbaResp, blockAckAgreementPolicy); if (acceptedByLocalPolicy) addbaRetryDeadlines.erase(std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())); else @@ -322,13 +323,14 @@ OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processR } } -void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp) +void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) { agreement->setIsAddbaResponseReceived(true); agreement->setIsAMsduSupported(addbaResp->getAMsduSupported()); agreement->setIsDelayedBlockAckPolicySupported(addbaResp->getBlockAckPolicy() == 0); agreement->setBufferSize(addbaResp->getBufferSize()); agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue()); + agreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaResp->getTransmitterAddress())); agreement->calculateExpirationTime(); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 5fa2c7b5d3a..33590702fe7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -31,8 +31,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); virtual uint8_t allocateDialogToken(); - virtual void createAgreement(const Ptr& addbaRequest, uint64_t transactionId); - virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); + virtual void createAgreement(const Ptr& addbaRequest, uint64_t transactionId, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); + virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); virtual OriginatorBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 740d18a0033..802a959d3f7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -26,6 +26,9 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage) blockAckTimeoutValue = par("blockAckTimeoutValue"); addbaResponseTimeout = par("addbaResponseTimeout"); addbaRetryBackoff = par("addbaRetryBackoff"); + localCompressedBlockAckSupported = par("localCompressedBlockAckSupported"); + for (const auto& address : cStringTokenizer(par("compressedBlockAckPeerAddresses")).asVector()) + compressedBlockAckPeerAddresses.insert(MacAddress(address.c_str())); if (addbaResponseTimeout <= 0) throw cRuntimeError("addbaResponseTimeout must be greater than zero"); if (addbaRetryBackoff < 0) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index 04c67ec3af8..c2ae595977e 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -27,6 +27,8 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetModuleBase, pub simtime_t blockAckTimeoutValue = -1; simtime_t addbaResponseTimeout = -1; simtime_t addbaRetryBackoff = -1; + bool localCompressedBlockAckSupported = false; + std::set compressedBlockAckPeerAddresses; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } @@ -43,6 +45,7 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetModuleBase, pub virtual bool isMsduSupported() const override { return aMsduSupported; } virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool isDelayedAckPolicySupported() const override { return delayedAckPolicySupported; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override { return localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.find(peerAddress) != compressedBlockAckPeerAddresses.end(); } virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index 8fa02193718..4f79b6cc3cb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -26,6 +26,8 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator double addbaResponseTimeout @unit(s) = default(1s); // Model response-wait timeout after the ADDBA Request is transmitted double addbaRetryBackoff @unit(s) = default(1s); // Model retry suppression after a failed or discarded ADDBA transaction; not an IEEE 802.11 timer + bool localCompressedBlockAckSupported = default(false); + string compressedBlockAckPeerAddresses = default(""); @display("i=block/control"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc index bc724cc5c94..bcacac4a1a4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -13,7 +13,6 @@ namespace inet { namespace ieee80211 { RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime, uint64_t generationId) : - startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), blockAckTimeoutValue(lastUsedTime), generationId(generationId) @@ -22,17 +21,16 @@ RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddr blockAckRecord = new BlockAckRecord(originatorAddress, tid, startingSequenceNumber); } -void RecipientBlockAckAgreement::blockAckPolicyFrameReceived(const Ptr& header) +void RecipientBlockAckAgreement::dataFrameReceived(const Ptr& header) { - ASSERT(header->getAckPolicy() == BLOCK_ACK); - blockAckRecord->blockAckPolicyFrameReceived(header); + blockAckRecord->dataFrameReceived(header, bufferSize); } std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement) { os << "originator address = " << agreement.blockAckRecord->getOriginatorAddress() << ", " << "tid = " << agreement.blockAckRecord->getTid() << ", " - << "starting sequence number = " << agreement.startingSequenceNumber << ", " + << "starting sequence number = " << agreement.getStartingSequenceNumber() << ", " << "buffer size = " << agreement.bufferSize << ", " << "block ack timeout value = " << agreement.blockAckTimeoutValue; return os; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 9f4f54ebffc..f0d5a59e191 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -18,7 +18,6 @@ class INET_API RecipientBlockAckAgreement : public cObject protected: BlockAckRecord *blockAckRecord = nullptr; - SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; bool isAddbaResponseSent = false; @@ -33,12 +32,12 @@ class INET_API RecipientBlockAckAgreement : public cObject RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue, uint64_t generationId = 0); virtual ~RecipientBlockAckAgreement() { delete blockAckRecord; } - virtual void blockAckPolicyFrameReceived(const Ptr& header); + virtual void dataFrameReceived(const Ptr& header); virtual BlockAckRecord *getBlockAckRecord() const { return blockAckRecord; } virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } - virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } + virtual SequenceNumberCyclic getStartingSequenceNumber() const { return blockAckRecord->getStartingSequenceNumber(); } virtual uint64_t getGenerationId() const { return generationId; } virtual void calculateExpirationTime() { diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 2d365570a22..769372cadb5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -32,21 +32,42 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived { QosFrameProcessingResult result; ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); + ReorderBuffer framesToPassUp; + auto sequenceNumber = dataHeader->getSequenceNumber(); + auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); + bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; + if (advancesWindow) { + // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): move WinStartB so the + // future MPDU fits, preserving complete displaced MSDUs for delivery. + auto newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; + framesToPassUp = collectCompletePrecedingMpdus(receiveBuffer, newStartingSequenceNumber); + for (const auto& entry : framesToPassUp) + receiveBuffer->removeFrame(SequenceNumberCyclic(entry.first)); + // Any remaining displaced entries are incomplete and cannot be delivered. + receiveBuffer->dropFramesUntil(newStartingSequenceNumber); + receiveBuffer->setNextExpectedSequenceNumber(newStartingSequenceNumber); + } // The reception of QoS data frames using Normal Ack policy shall not be used by the // recipient to reset the timer to detect Block Ack timeout (see 10.5.4). // This allows the recipient to delete the Block Ack if the originator does not switch // back to using Block Ack. auto insertionResult = receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); if (insertionResult == ReceiveBuffer::FrameInsertionResult::INSERTED) { - if (dataHeader->getAckPolicy() == BLOCK_ACK) - agreement->blockAckPolicyFrameReceived(dataHeader); + agreement->dataFrameReceived(dataHeader); + if (advancesWindow) { + auto consecutiveCompleteMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); + releaseReceiveBuffer(receiveBuffer, consecutiveCompleteMpdus); + framesToPassUp.insert(framesToPassUp.end(), consecutiveCompleteMpdus.begin(), consecutiveCompleteMpdus.end()); + result.frames = framesToPassUp; + return result; + } auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); if (earliestCompleteMsduOrAMsdu.size() > 0) { auto earliestSequenceNumber = earliestCompleteMsduOrAMsdu.at(0)->peekAtFront()->getSequenceNumber(); // If, after an MPDU is received, the receive buffer is full, the complete MSDU or A-MSDU with the earliest // sequence number shall be passed up to the next MAC process. if (receiveBuffer->isFull()) { - passedUp(agreement, receiveBuffer, earliestSequenceNumber); + passedUp(receiveBuffer, earliestSequenceNumber); result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } @@ -54,7 +75,7 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived // A-MSDU in the buffer with the lowest sequence number is equal to the NextExpectedSequenceNumber for // that Block Ack agreement, then the MPDU shall be passed up to the next MAC process. else if (earliestSequenceNumber == receiveBuffer->getNextExpectedSequenceNumber()) { - passedUp(agreement, receiveBuffer, earliestSequenceNumber); + passedUp(receiveBuffer, earliestSequenceNumber); result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } @@ -66,8 +87,7 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived // received BLOCK_ACK frame, even though the fragment is rejected // by the tombstone and must be reported to the service for drop // observability. - if (dataHeader->getAckPolicy() == BLOCK_ACK) - agreement->blockAckPolicyFrameReceived(dataHeader); + agreement->dataFrameReceived(dataHeader); result.tombstonedFragments.push_back(dataPacket); return result; } @@ -104,6 +124,9 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq else { throw cRuntimeError("Multi-Tid BlockAckReq is currently an unimplemented feature"); } + // IEEE Std 802.11-2024, 10.25.6.3-10.25.6.5: adjust WinStartR + // from the BAR before generating the response, even without a receive buffer. + agreement->getBlockAckRecord()->advanceStartingSequenceNumber(startingSequenceNumber); auto id = std::make_pair(tid, blockAckReq->getTransmitterAddress()); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { @@ -117,19 +140,17 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq // the starting sequence number sequentially until there is an incomplete or missing MSDU // or A-MSDU in the buffer. auto consecutiveCompleteFollowingMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, startingSequenceNumber); - // If no MSDUs or A-MSDUs are passed up to the next MAC process after the receipt - // of the BlockAckReq frame and the starting sequence number of the BlockAckReq frame is newer than the - // NextExpectedSequenceNumber for that Block Ack agreement, then the NextExpectedSequenceNumber for - // that Block Ack agreement is set to the sequence number of the BlockAckReq frame. - int numOfMsdusToPassUp = completePrecedingMpdus.size() + consecutiveCompleteFollowingMpdus.size(); - if (numOfMsdusToPassUp == 0 && receiveBuffer->getNextExpectedSequenceNumber() < startingSequenceNumber) - receiveBuffer->setNextExpectedSequenceNumber(startingSequenceNumber); - // The recipient shall then release any buffers held by preceding MPDUs. - releaseReceiveBuffer(agreement, receiveBuffer, completePrecedingMpdus); - releaseReceiveBuffer(agreement, receiveBuffer, consecutiveCompleteFollowingMpdus); // The recipient shall pass MSDUs and A-MSDUs up to the next MAC process in order of increasing sequence // number. - completePrecedingMpdus.insert(consecutiveCompleteFollowingMpdus.begin(), consecutiveCompleteFollowingMpdus.end()); + completePrecedingMpdus.insert(completePrecedingMpdus.end(), consecutiveCompleteFollowingMpdus.begin(), consecutiveCompleteFollowingMpdus.end()); + // Detach all packets being returned before releasing stale buffered entries. + releaseReceiveBuffer(receiveBuffer, completePrecedingMpdus); + // Release any remaining buffers held by incomplete preceding MPDUs, then + // advance NextExpectedSequenceNumber to at least the BAR SSN without + // regressing it past consecutively released MSDUs. + receiveBuffer->dropFramesUntil(startingSequenceNumber); + if (receiveBuffer->getNextExpectedSequenceNumber() < startingSequenceNumber) + receiveBuffer->setNextExpectedSequenceNumber(startingSequenceNumber); return completePrecedingMpdus; } return ReorderBuffer(); @@ -144,12 +165,14 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp { ReorderBuffer completePrecedingMpdus; const auto& buffer = receiveBuffer->getBuffer(); - for (auto it : buffer) { // collects complete preceding MPDUs - auto sequenceNumber = it.first; - auto fragments = it.second; - if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) - if (ReceiveBuffer::isComplete(fragments)) - completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); + auto currentStartingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); + for (int i = 0; i < receiveBuffer->getBufferSize(); i++) { + auto sequenceNumber = currentStartingSequenceNumber + i; + if (!(sequenceNumber < startingSequenceNumber)) + break; + auto it = buffer.find(sequenceNumber.get()); + if (it != buffer.end() && ReceiveBuffer::isComplete(it->second)) + completePrecedingMpdus.push_back(std::make_pair(sequenceNumber.get(), sortFragmentsByFragmentNumber(it->second))); } return completePrecedingMpdus; } @@ -176,18 +199,22 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder if (it != buffer.end()) { auto fragments = it->second; if (ReceiveBuffer::isComplete(fragments)) { - reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); + reorderBuffer.push_back(std::make_pair(seqNum.get(), sortFragmentsByFragmentNumber(fragments))); return true; } } return false; } -void BlockAckReordering::releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer) +void BlockAckReordering::releaseReceiveBuffer(ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer) { - for (auto it : reorderBuffer) { - auto sequenceNumber = it.first; - passedUp(agreement, receiveBuffer, SequenceNumberCyclic(sequenceNumber)); + // Detach all packets whose ownership is returned before stale-buffer cleanup. + for (const auto& entry : reorderBuffer) + receiveBuffer->removeFrame(SequenceNumberCyclic(entry.first)); + for (const auto& entry : reorderBuffer) { + auto sequenceNumber = entry.first; + receiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(sequenceNumber) + 1); + receiveBuffer->dropFramesUntil(SequenceNumberCyclic(sequenceNumber)); } } @@ -244,7 +271,7 @@ std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress return frames; } -void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber) +void BlockAckReordering::passedUp(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber) { // Each time that the recipient passes an MSDU or A-MSDU for a Block Ack agreement up to the next MAC // process, the NextExpectedSequenceNumber for that Block Ack agreement is set to the sequence number of the @@ -252,7 +279,6 @@ void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, Receive receiveBuffer->setNextExpectedSequenceNumber(sequenceNumber + 1); receiveBuffer->dropFramesUntil(sequenceNumber); receiveBuffer->removeFrame(sequenceNumber); - agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); } std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index c754485d688..ac3ace0a951 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -24,7 +24,7 @@ class INET_API BlockAckReordering { public: typedef std::vector Fragments; - typedef std::map ReorderBuffer; + typedef std::vector> ReorderBuffer; // Values in ReorderBuffer results from processReceivedQoSFrame() and // processReceivedBlockAckReq() contain complete fragment vectors ordered @@ -41,8 +41,8 @@ class INET_API BlockAckReordering ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); std::vector getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer); - void passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber); - void releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer); + void passedUp(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber); + void releaseReceiveBuffer(ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer); ReceiveBuffer *createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement); bool addMsduIfComplete(ReceiveBuffer *receiveBuffer, ReorderBuffer& reorderBuffer, SequenceNumberCyclic seqNum); diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index facc4d5791c..1c0293f4c0a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -30,6 +30,7 @@ class INET_API IOriginatorBlockAckAgreementPolicy virtual simtime_t computeAddbaRetryBackoff() const = 0; virtual simtime_t getBlockAckTimeoutValue() const = 0; virtual bool isDelayedAckPolicySupported() const = 0; + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const = 0; virtual int getMaximumAllowedBufferSize() const = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index a1c748d4451..626a08819be 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -23,7 +23,6 @@ void OriginatorQosAckPolicy::initialize(int stage) rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); maxBlockAckPolicyFrameLength = par("maxBlockAckPolicyFrameLength"); blockAckReqThreshold = par("blockAckReqThreshold"); - assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); blockAckTimeout = par("blockAckTimeout"); ackTimeout = par("ackTimeout"); } @@ -64,16 +63,15 @@ SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const { - return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupportsCompressedBlockAck); + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement); } -bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) { // IEEE Std 802.11-2024, Table 11-8 and 10.25.6.1: use the compressed // variant only for an established immediate HT Block Ack agreement. - // Peer HT capability is not represented by the baseline agreement contract; - // the parameter is an explicit assumption supplied by the configuration. - if (!assumePeerSupportsCompressedBlockAck || agreement == nullptr || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) + // The agreement snapshots peer capability state when it is established. + if (agreement == nullptr || !agreement->getIsCompressedBlockAckSupported() || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) return false; bool hasMatchingOutstandingFrame = false; for (auto frame : outstandingFrames) { diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index 64e2eb62664..e6209e02560 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -22,7 +22,6 @@ class INET_API OriginatorQosAckPolicy : public ModeSetModuleBase, public IOrigin IQosRateSelection *rateSelection = nullptr; int maxBlockAckPolicyFrameLength = -1; int blockAckReqThreshold = -1; - bool assumePeerSupportsCompressedBlockAck = false; simtime_t blockAckTimeout = -1; simtime_t ackTimeout = -1; @@ -34,7 +33,7 @@ class INET_API OriginatorQosAckPolicy : public ModeSetModuleBase, public IOrigin virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const; virtual std::map, std::vector> getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const; virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; - static bool isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); + static bool isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement); public: virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned index 0eaa3b68a88..d4e0dceb93d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned @@ -22,9 +22,6 @@ simple OriginatorQosAckPolicy extends SimpleModule like IOriginatorQosAckPolicy int blockAckReqThreshold = default(5); int maxBlockAckPolicyFrameLength @unit(B) = default(1000B); - // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. - // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. - bool assumePeerSupportsCompressedBlockAck = default(false); double blockAckTimeout @unit(s) = default(-1s); double ackTimeout @unit(s) = default(-1s); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index b8ccc55eb88..3e81c4cb221 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -75,12 +75,11 @@ bool RecipientQosAckPolicy::isBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) { - // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5: an addressed, syntactically - // valid Compressed BlockAckReq elicits a Compressed BlockAck, including a null response. + // IEEE Std 802.11-2024, 10.25.6.4 and 10.25.6.5: a null response + // requires an established HT-immediate agreement whose partial state is absent. if (blockAckReq->getFragmentNumber() != 0) return false; - // A missing partial state still elicits the mandatory null compressed BA. - return agreement == nullptr || (agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported()); + return agreement != nullptr && agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported(); } // diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index b1222c6e72b..3db5e4117ce 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -152,7 +152,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data return std::vector(); } BlockAckReordering::ReorderBuffer frames; - frames[dataHeader->getSequenceNumber().get()].push_back(dataPacket); + frames.push_back(std::make_pair(dataHeader->getSequenceNumber().get(), BlockAckReordering::Fragments({dataPacket}))); if (blockAckReordering && blockAckAgreementHandler) { Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test index 8dde5158a5c..039df324eb4 100644 --- a/tests/module/Ieee80211CompressedBlockAckRuntime.test +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -22,9 +22,12 @@ abstract = false sim-time-limit = 1.05s **.opMode = "n(mixed-2.4Ghz)" -# The originator explicitly opts in based on its peer-capability assumption; -# the recipient requires no additional station-wide capability setting. -**.originatorAckPolicy.assumePeerSupportsCompressedBlockAck = true +**.originatorBlockAckAgreementPolicy.localCompressedBlockAckSupported = true +*.cliHost.wlan[*].address = "10:00:00:00:00:01" +*.srvHost.wlan[*].address = "10:00:00:00:00:02" +*.cliHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" +*.ap.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:01 10:00:00:00:00:02" +*.srvHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" **.cmdenv-log-level = info %contains: stdout diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 87e4a03544c..0063793843d 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -74,6 +74,7 @@ class TestOriginatorPolicy : public IOriginatorBlockAckAgreementPolicy virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } virtual simtime_t getBlockAckTimeoutValue() const override { return 0; } virtual bool isDelayedAckPolicySupported() const override { return false; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress&) const override { return false; } virtual int getMaximumAllowedBufferSize() const override { return 64; } }; @@ -103,7 +104,7 @@ class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosA virtual bool isAckNeeded(const Ptr&) const override { return true; } virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *agreement) const override { return blockAckNeeded && agreement != nullptr; } virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } - virtual simtime_t computeBasicBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } + virtual simtime_t computeBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } }; class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback @@ -2618,7 +2619,8 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; ASSERT(frames.empty()); } ASSERT(immediateFrames.size() == 1); - auto immediateFragments = immediateFrames.at(100); + ASSERT(immediateFrames.front().first == 100); + auto immediateFragments = immediateFrames.front().second; assertFragmentOrder(immediateFragments); reassembleOrderedFragments(immediateFragments, { 0x40, 0x41, 0x42, 0x43 }); @@ -2636,7 +2638,8 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; barRequest->setStartingSequenceNumber(SequenceNumberCyclic(201)); auto barFrames = barReordering.processReceivedBlockAckReq(&barAgreement, barRequest); ASSERT(barFrames.size() == 1); - auto barFragments = barFrames.at(201); + ASSERT(barFrames.front().first == 201); + auto barFragments = barFrames.front().second; assertFragmentOrder(barFragments); reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 8d34b512d95..e0bbc85340e 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -5,10 +5,13 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. %includes: #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" @@ -28,15 +31,50 @@ class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler { public: + using OriginatorBlockAckAgreementHandler::createAgreement; using OriginatorBlockAckAgreementHandler::updateAgreement; }; +class TestOriginatorBlockAckAgreementPolicy : public OriginatorBlockAckAgreementPolicy +{ + public: + void configureCompressedBlockAckCapability(bool localSupport, std::initializer_list peerAddresses) + { + localCompressedBlockAckSupported = localSupport; + compressedBlockAckPeerAddresses = std::set(peerAddresses); + } +}; + +class TestBlockAckRecord : public BlockAckRecord +{ + public: + TestBlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) : + BlockAckRecord(originatorAddress, tid, startingSequenceNumber) + { + } + + bool hasRecordedAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber) const + { + return acknowledgmentState.find(SequenceControlField(sequenceNumber.get(), fragmentNumber)) != acknowledgmentState.end(); + } +}; + +class TestBlockAckReordering : public BlockAckReordering +{ + public: + ReceiveBuffer *getReceiveBuffer(Tid tid, const MacAddress& originatorAddress) const + { + auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; + class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy { public: - static bool isCompressedRequestNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupport) + static bool isCompressedRequestNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) { - return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupport); + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement); } }; @@ -91,6 +129,18 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag return new Packet("outstanding", header); } +static std::pair> makeReceivedQosFrame(SequenceNumber sequenceNumber, FragmentNumber fragmentNumber = 0, bool moreFragments = false, AckPolicy ackPolicy = BLOCK_ACK) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setAckPolicy(ackPolicy); + header->setTid(5); + header->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + return std::make_pair(new Packet("received", header), header); +} + %activity: { auto request = makeCompressedBlockAckReq(); @@ -145,14 +195,81 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement emptyAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + emptyAgreement.addbaResposneSent(); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &emptyAgreement)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &emptyAgreement)); for (int i = 0; i < 64; i++) ASSERT(!response->getBlockAckBitmap().getBit(i)); EV << "Empty recipient record produces a null compressed BA.\n"; } +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto request = makeShared(); + request->setTransmitterAddress(MacAddress("11:22:33:44:55:66")); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (int i = 0; i < 64; i++) + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(!response->getBlockAckBitmap(i).getBit(fragmentNumber)); + agreement.getBlockAckRecord()->advanceStartingSequenceNumber(SequenceNumberCyclic(101)); + response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(response->getBlockAckBitmap(0).getBit(fragmentNumber)); + for (int i = 1; i < 64; i++) + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(!response->getBlockAckBitmap(i).getBit(fragmentNumber)); + EV << "Empty Basic BA distinguishes old and current-window MPDUs.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + for (int startingSequenceNumber : {1985, 1986}) { + BlockAckReordering reordering; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(startingSequenceNumber)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (int i = 0; i < 64; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(startingSequenceNumber)); + } + + BlockAckReordering reordering; + RecipientBlockAckAgreement wrappedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4090), 64, SIMTIME_ZERO); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(5)); + reordering.processReceivedBlockAckReq(&wrappedAgreement, request); + ASSERT(wrappedAgreement.getStartingSequenceNumber() == SequenceNumberCyclic(5)); + ASSERT(wrappedAgreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(4095))); + ASSERT(!wrappedAgreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(5))); + EV << "BAR advances the authoritative receive window across boundaries and wrap.\n"; +} + +{ + TestBlockAckRecord record(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100)); + auto received102 = makeReceivedQosFrame(102); + auto received105 = makeReceivedQosFrame(105); + record.dataFrameReceived(received102.second, 64); + record.dataFrameReceived(received105.second, 64); + ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); + ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(105), 0)); + record.advanceStartingSequenceNumber(SequenceNumberCyclic(104)); + ASSERT(!record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); + ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(105), 0)); + delete received102.first; + delete received105.first; + EV << "Receive-window advancement erases old state and retains overlap.\n"; +} + { TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(101), 64, SIMTIME_ZERO); @@ -163,7 +280,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); } auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(101)); @@ -183,7 +300,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(102)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(100)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); @@ -193,6 +310,184 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag EV << "Compressed bitmap preserves leading receive-window holes.\n"; } +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestRecipientBlockAckProcedure procedure; + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto incomplete0 = makeReceivedQosFrame(0, 0, true); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete0.first, incomplete0.second).empty()); + auto received64 = makeReceivedQosFrame(64); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received64.first, received64.second).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer != nullptr); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(receiveBuffer->getBuffer().find(0) == receiveBuffer->getBuffer().end()); + ASSERT(receiveBuffer->getBuffer().find(64) != receiveBuffer->getBuffer().end()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(1)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (int i = 0; i < 63; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + ASSERT(response->getBlockAckBitmap().getBit(63)); + EV << "SN64 advances a size-64 scoreboard and remains buffered across a gap.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto received1 = makeReceivedQosFrame(1); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received1.first, received1.second).empty()); + auto received2 = makeReceivedQosFrame(2); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received2.first, received2.second).empty()); + auto received65 = makeReceivedQosFrame(65); + auto framesToPassUp = reordering.processReceivedQoSFrame(&agreement, received65.first, received65.second); + ASSERT(framesToPassUp.size() == 2); + ASSERT(framesToPassUp[0].first == 1); + ASSERT(framesToPassUp[1].first == 2); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(3)); + ASSERT(receiveBuffer->getBuffer().find(65) != receiveBuffer->getBuffer().end()); + for (const auto& entry : framesToPassUp) + for (auto packet : entry.second) + delete packet; + EV << "Future MPDU returns displaced and consecutive complete MSDUs without losing gaps.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestRecipientBlockAckProcedure procedure; + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto normalAck64 = makeReceivedQosFrame(64, 0, false, NORMAL_ACK); + ASSERT(reordering.processReceivedQoSFrame(&agreement, normalAck64.first, normalAck64.second).empty()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(1)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(response->getBlockAckBitmap().getBit(63)); + EV << "Normal-Ack data advances and updates the Block Ack scoreboard.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto normalAck100 = makeReceivedQosFrame(100, 0, false, NORMAL_ACK); + auto framesToPassUp = reordering.processReceivedQoSFrame(&agreement, normalAck100.first, normalAck100.second); + ASSERT(framesToPassUp.size() == 1 && framesToPassUp[0].first == 100); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(100)); + ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + delete framesToPassUp[0].second[0]; + EV << "Upward delivery advances only the reorder cursor, not WinStartR.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(4090), 64, SIMTIME_ZERO); + auto received4095 = makeReceivedQosFrame(4095); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received4095.first, received4095.second).empty()); + auto received0 = makeReceivedQosFrame(0); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received0.first, received0.second).empty()); + auto received62 = makeReceivedQosFrame(62); + auto framesToPassUp = reordering.processReceivedQoSFrame(&agreement, received62.first, received62.second); + ASSERT(framesToPassUp.size() == 2); + ASSERT(framesToPassUp[0].first == 4095); + ASSERT(framesToPassUp[1].first == 0); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(receiveBuffer->getBuffer().find(62) != receiveBuffer->getBuffer().end()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(4095)); + for (const auto& entry : framesToPassUp) + for (auto packet : entry.second) + delete packet; + EV << "Data-window slide preserves cyclic 4095-to-0 delivery order.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(4090), 64, SIMTIME_ZERO); + auto received4095 = makeReceivedQosFrame(4095); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received4095.first, received4095.second).empty()); + auto received0 = makeReceivedQosFrame(0); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received0.first, received0.second).empty()); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(1)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(framesToPassUp.size() == 2); + ASSERT(framesToPassUp[0].first == 4095); + ASSERT(framesToPassUp[1].first == 0); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + for (const auto& entry : framesToPassUp) + for (auto packet : entry.second) + delete packet; + EV << "BAR release preserves cyclic order without advancing WinStartR past its SSN.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto received101 = makeReceivedQosFrame(101); + auto firstResult = reordering.processReceivedQoSFrame(&agreement, received101.first, received101.second); + ASSERT(firstResult.empty()); + auto duplicate101 = makeReceivedQosFrame(101); + auto duplicateResult = reordering.processReceivedQoSFrame(&agreement, duplicate101.first, duplicate101.second); + ASSERT(duplicateResult.empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getBuffer().at(101).size() == 1); + ASSERT(receiveBuffer->getBuffer().at(101)[0] == received101.first); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(102)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(framesToPassUp.size() == 1 && framesToPassUp[0].first == 101); + ASSERT(framesToPassUp[0].second[0] == received101.first); + delete framesToPassUp[0].second[0]; + EV << "Duplicate insertion preserves ownership of the original buffered MPDU.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto incomplete101 = makeReceivedQosFrame(101, 0, true); + auto insertionResult = reordering.processReceivedQoSFrame(&agreement, incomplete101.first, incomplete101.second); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(102)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + if (!insertionResult.empty() || !framesToPassUp.empty() || !receiveBuffer->getBuffer().empty() || receiveBuffer->getLength() != 0 || + receiveBuffer->getNextExpectedSequenceNumber() != SequenceNumberCyclic(102) || agreement.getStartingSequenceNumber() != SequenceNumberCyclic(102)) + throw cRuntimeError("BAR did not discard an incomplete preceding MPDU or advance both receive-window starts to its SSN"); + EV << "BAR discards incomplete preceding MPDUs and advances the reorder cursor.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto complete101 = makeReceivedQosFrame(101); + auto completeInsertionResult = reordering.processReceivedQoSFrame(&agreement, complete101.first, complete101.second); + auto incomplete102 = makeReceivedQosFrame(102, 0, true); + auto incompleteInsertionResult = reordering.processReceivedQoSFrame(&agreement, incomplete102.first, incomplete102.second); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(103)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + if (!completeInsertionResult.empty() || !incompleteInsertionResult.empty() || framesToPassUp.size() != 1 || framesToPassUp[0].first != 101 || + framesToPassUp[0].second.size() != 1 || framesToPassUp[0].second[0] != complete101.first || !receiveBuffer->getBuffer().empty() || + receiveBuffer->getLength() != 0 || receiveBuffer->getNextExpectedSequenceNumber() != SequenceNumberCyclic(103) || + agreement.getStartingSequenceNumber() != SequenceNumberCyclic(103)) + throw cRuntimeError("BAR did not return a complete preceding MPDU and discard the incomplete gap that followed it"); + delete framesToPassUp[0].second[0]; + EV << "BAR releases complete preceding MPDUs and discards a following incomplete gap.\n"; +} + { TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4095), 64, SIMTIME_ZERO); @@ -203,7 +498,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); } auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); @@ -223,7 +518,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(1)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); @@ -235,7 +530,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { auto request = makeCompressedBlockAckReq(); - ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(1); ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(0); @@ -249,23 +544,70 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag EV << "Recipient fragment and agreement-policy gates passed.\n"; } +{ + MacAddress listedPeer("10:20:30:40:50:60"); + MacAddress unlistedPeer("10:20:30:40:50:61"); + MacAddress positivePeer("10:20:30:40:50:62"); + TestOriginatorBlockAckAgreementPolicy localDisabledPolicy; + localDisabledPolicy.configureCompressedBlockAckCapability(false, {listedPeer}); + TestOriginatorBlockAckAgreementPolicy localEnabledPolicy; + localEnabledPolicy.configureCompressedBlockAckCapability(true, {listedPeer, positivePeer}); + ASSERT(!localDisabledPolicy.isPeerCompressedBlockAckSupported(listedPeer)); + ASSERT(!localEnabledPolicy.isPeerCompressedBlockAckSupported(unlistedPeer)); + ASSERT(localEnabledPolicy.isPeerCompressedBlockAckSupported(listedPeer)); + + TestOriginatorBlockAckAgreementHandler handler; + auto makeAddbaRequest = [](const MacAddress& receiverAddress) { + auto request = makeShared(); + request->setReceiverAddress(receiverAddress); + request->setTid(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + return request; + }; + handler.createAgreement(makeAddbaRequest(listedPeer), &localDisabledPolicy); + handler.createAgreement(makeAddbaRequest(unlistedPeer), &localEnabledPolicy); + handler.createAgreement(makeAddbaRequest(positivePeer), &localEnabledPolicy); + ASSERT(!handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); + ASSERT(!handler.getAgreement(unlistedPeer, 5)->getIsCompressedBlockAckSupported()); + ASSERT(handler.getAgreement(positivePeer, 5)->getIsCompressedBlockAckSupported()); + + auto response = makeShared(); + response->setTransmitterAddress(listedPeer); + response->setBlockAckPolicy(true); + response->setBufferSize(64); + response->setBlockAckTimeoutValue(SIMTIME_ZERO); + handler.updateAgreement(handler.getAgreement(listedPeer, 5), response, &localEnabledPolicy); + ASSERT(handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); + EV << "Local and per-peer capability gates are snapshotted by agreements.\n"; +} + { MacAddress receiverAddress("10:20:30:40:50:60"); OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); immediateAgreement.setIsAddbaResponseReceived(true); std::vector outstandingFrames { makeOutstandingQosFrame(receiverAddress, 5) }; - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, false)); - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr, true)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr)); + immediateAgreement.setIsCompressedBlockAckSupported(true); OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true); delayedAgreement.setIsAddbaResponseReceived(true); - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement, true)); - ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delayedAgreement.setIsCompressedBlockAckSupported(true); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement)); + ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); + MacAddress legacyReceiverAddress("10:20:30:40:50:61"); + OriginatorBlockAckAgreement legacyAgreement(legacyReceiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + legacyAgreement.setIsAddbaResponseReceived(true); + std::vector legacyOutstandingFrames { makeOutstandingQosFrame(legacyReceiverAddress, 5) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(legacyOutstandingFrames, &legacyAgreement)); + delete legacyOutstandingFrames[0]; delete outstandingFrames[0]; outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 1) }; - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); delete outstandingFrames[0]; outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 0, true) }; - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); delete outstandingFrames[0]; EV << "Originator capability, agreement, and fragmentation gates passed.\n"; } @@ -301,12 +643,15 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { TestOriginatorBlockAckAgreementHandler handler; + TestOriginatorBlockAckAgreementPolicy policy; + policy.configureCompressedBlockAckCapability(false, {}); OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false); auto delayedResponse = makeShared(); delayedResponse->setBlockAckPolicy(false); delayedResponse->setBufferSize(64); delayedResponse->setBlockAckTimeoutValue(SIMTIME_ZERO); - handler.updateAgreement(&agreement, delayedResponse); + delayedResponse->setTransmitterAddress(MacAddress("10:20:30:40:50:60")); + handler.updateAgreement(&agreement, delayedResponse, &policy); ASSERT(agreement.getIsAddbaResponseReceived()); ASSERT(agreement.getIsDelayedBlockAckPolicySupported()); EV << "Accepted delayed agreement is retained for Basic BAR fallback.\n"; @@ -342,11 +687,24 @@ Compressed BAR encoding and round-trip passed. Compressed BA encoding and round-trip passed. Missing recipient state produces a null compressed BA. Empty recipient record produces a null compressed BA. +Empty Basic BA distinguishes old and current-window MPDUs. +BAR advances the authoritative receive window across boundaries and wrap. +Receive-window advancement erases old state and retains overlap. Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. +SN64 advances a size-64 scoreboard and remains buffered across a gap. +Future MPDU returns displaced and consecutive complete MSDUs without losing gaps. +Normal-Ack data advances and updates the Block Ack scoreboard. +Upward delivery advances only the reorder cursor, not WinStartR. +Data-window slide preserves cyclic 4095-to-0 delivery order. +BAR release preserves cyclic order without advancing WinStartR past its SSN. +Duplicate insertion preserves ownership of the original buffered MPDU. +BAR discards incomplete preceding MPDUs and advances the reorder cursor. +BAR releases complete preceding MPDUs and discards a following incomplete gap. Compressed bitmap wraps from sequence 4095 to 0. Compressed bitmap preserves leading holes across sequence wrap. Recipient fragment and agreement-policy gates passed. +Local and per-peer capability gates are snapshotted by agreements. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. From bdff56dfb6286596266cf2d1e672792cba9c6bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 13:03:04 +0200 Subject: [PATCH 64/91] fix(ieee80211): make Block Ack receive processing transactional Update the Block Ack scoreboard for every related received QoS Data MPDU, including frames using Normal Ack, independently of reorder-buffer admission. Handle the receive-window cases explicitly: ignore old sequence numbers, record in-window MPDUs, and advance WinStartR before recording MPDUs beyond WinEndR. This behavior deliberately applies to both Basic and Compressed Block Ack agreements, as required by IEEE 802.11-2024 sections 10.25.6.3 and 10.25.6.4. Make far-ahead reorder-window movement transactional. Validate and insert the incoming MPDU against the proposed WinStartB before releasing displaced MSDUs or changing NextExpectedSequenceNumber. If admission fails, discard only the incoming packet and preserve the reorder window and its buffered frames, while retaining the independently required scoreboard update. Calculate receive-buffer capacity after accounting for entries displaced by the proposed window. Count each fragment as one buffer slot, allow an advancing MPDU to reuse slots that will be reclaimed, and remove incomplete stale entries only after successful admission. Preserve delivery of complete displaced and consecutive MSDUs, including across the 4095-to-0 sequence number boundary. Centralize supported one-TID BlockAckReq classification and extraction of the variant, TID, and starting sequence number. Use the shared classifier in HCF, RecipientQosMacDataService, and BlockAckReordering so only Basic and Compressed requests reach the implemented paths. Multi-TID requests remain unsupported and follow the existing rejection behavior. Extend the focused Block Ack tests to cover old scoreboard inputs, Normal-Ack behavior with Basic Block Ack, fragment-full admission failure, successful slot reclamation across sequence wrap, real HCF dispatch through a procedure spy, and consistent Basic, Compressed, and Multi-TID request classification. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 8 +- .../mac/blockack/OneTidBlockAckReqVariant.h | 46 ++++++ .../blockackreordering/BlockAckReordering.cc | 108 +++++++------ .../mac/blockackreordering/ReceiveBuffer.cc | 89 +++++++---- .../mac/blockackreordering/ReceiveBuffer.h | 3 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 7 +- .../recipient/RecipientQosMacDataService.cc | 16 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 144 +++++++++++++++++- 8 files changed, 313 insertions(+), 108 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 14a21493581..e212ed19537 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -23,8 +23,12 @@ void BlockAckRecord::dataFrameReceived(const Ptr& hea { SequenceNumberCyclic sequenceNumber = header->getSequenceNumber(); FragmentNumber fragmentNumber = header->getFragmentNumber(); - // IEEE Std 802.11-2024, 10.25.6.3(b) and 10.25.6.4(c): a related - // MPDU beyond WinEndR advances the receive window before its bit is set. + // IEEE Std 802.11-2024, 10.25.6.3(b), case 3: an old related MPDU + // does not change the Block Ack record. + if (!(startingSequenceNumber <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048)) + return; + // Cases 1 and 2: record an in-window MPDU, or advance WinStartR + // before recording an MPDU beyond WinEndR (also see 10.25.6.4(c)). if (startingSequenceNumber + windowSize <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048) advanceStartingSequenceNumber(sequenceNumber - windowSize + 1); acknowledgmentState[SequenceControlField(sequenceNumber.get(), fragmentNumber)] = true; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h new file mode 100644 index 00000000000..0d158b365cf --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -0,0 +1,46 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_ONETIDBLOCKACKREQVARIANT_H +#define __INET_ONETIDBLOCKACKREQVARIANT_H + +#include + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +namespace inet { +namespace ieee80211 { + +enum class OneTidBlockAckReqVariant +{ + BASIC, + COMPRESSED, +}; + +struct OneTidBlockAckReqDetails +{ + Ptr blockAckReq; + OneTidBlockAckReqVariant variant; + Tid tid; + SequenceNumberCyclic startingSequenceNumber; +}; + +inline std::optional getOneTidBlockAckReqDetails(const Ptr& header) +{ + if (auto basicBlockAckReq = dynamicPtrCast(header)) + return OneTidBlockAckReqDetails { basicBlockAckReq, OneTidBlockAckReqVariant::BASIC, + static_cast(basicBlockAckReq->getTidInfo()), basicBlockAckReq->getStartingSequenceNumber() }; + else if (auto compressedBlockAckReq = dynamicPtrCast(header)) + return OneTidBlockAckReqDetails { compressedBlockAckReq, OneTidBlockAckReqVariant::COMPRESSED, + static_cast(compressedBlockAckReq->getTidInfo()), compressedBlockAckReq->getStartingSequenceNumber() }; + else + return std::nullopt; +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 769372cadb5..2ec88f1ccd0 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -9,6 +9,7 @@ #include +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { @@ -36,10 +37,31 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived auto sequenceNumber = dataHeader->getSequenceNumber(); auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; + // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: update the + // scoreboard for every related Data frame, independently of reorder storage. + agreement->dataFrameReceived(dataHeader); + SequenceNumberCyclic newStartingSequenceNumber; + if (advancesWindow) { + // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): store the future MPDU + // before moving WinStartB and releasing complete displaced MSDUs. + newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; + } + // The reception of QoS data frames using Normal Ack policy shall not be used by the + // recipient to reset the timer to detect Block Ack timeout (see 10.5.4). + // This allows the recipient to delete the Block Ack if the originator does not switch + // back to using Block Ack. Use the proposed window for a future MPDU so admission + // can reclaim entries that will be displaced. + auto insertionResult = advancesWindow ? + receiveBuffer->insertFrameWithResult(dataPacket, dataHeader, newStartingSequenceNumber) : + receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); + if (insertionResult != ReceiveBuffer::FrameInsertionResult::INSERTED) { + if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) + result.tombstonedFragments.push_back(dataPacket); + else + delete dataPacket; + return result; + } if (advancesWindow) { - // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): move WinStartB so the - // future MPDU fits, preserving complete displaced MSDUs for delivery. - auto newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; framesToPassUp = collectCompletePrecedingMpdus(receiveBuffer, newStartingSequenceNumber); for (const auto& entry : framesToPassUp) receiveBuffer->removeFrame(SequenceNumberCyclic(entry.first)); @@ -47,52 +69,33 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived receiveBuffer->dropFramesUntil(newStartingSequenceNumber); receiveBuffer->setNextExpectedSequenceNumber(newStartingSequenceNumber); } - // The reception of QoS data frames using Normal Ack policy shall not be used by the - // recipient to reset the timer to detect Block Ack timeout (see 10.5.4). - // This allows the recipient to delete the Block Ack if the originator does not switch - // back to using Block Ack. - auto insertionResult = receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); - if (insertionResult == ReceiveBuffer::FrameInsertionResult::INSERTED) { - agreement->dataFrameReceived(dataHeader); - if (advancesWindow) { - auto consecutiveCompleteMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); - releaseReceiveBuffer(receiveBuffer, consecutiveCompleteMpdus); - framesToPassUp.insert(framesToPassUp.end(), consecutiveCompleteMpdus.begin(), consecutiveCompleteMpdus.end()); - result.frames = framesToPassUp; + if (advancesWindow) { + auto consecutiveCompleteMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); + releaseReceiveBuffer(receiveBuffer, consecutiveCompleteMpdus); + framesToPassUp.insert(framesToPassUp.end(), consecutiveCompleteMpdus.begin(), consecutiveCompleteMpdus.end()); + result.frames = framesToPassUp; + return result; + } + auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); + if (earliestCompleteMsduOrAMsdu.size() > 0) { + auto earliestSequenceNumber = earliestCompleteMsduOrAMsdu.at(0)->peekAtFront()->getSequenceNumber(); + // If, after an MPDU is received, the receive buffer is full, the complete MSDU or A-MSDU with the earliest + // sequence number shall be passed up to the next MAC process. + if (receiveBuffer->isFull()) { + passedUp(receiveBuffer, earliestSequenceNumber); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } - auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); - if (earliestCompleteMsduOrAMsdu.size() > 0) { - auto earliestSequenceNumber = earliestCompleteMsduOrAMsdu.at(0)->peekAtFront()->getSequenceNumber(); - // If, after an MPDU is received, the receive buffer is full, the complete MSDU or A-MSDU with the earliest - // sequence number shall be passed up to the next MAC process. - if (receiveBuffer->isFull()) { - passedUp(receiveBuffer, earliestSequenceNumber); - result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); - return result; - } - // If, after an MPDU is received, the receive buffer is not full, but the sequence number of the complete MSDU or - // A-MSDU in the buffer with the lowest sequence number is equal to the NextExpectedSequenceNumber for - // that Block Ack agreement, then the MPDU shall be passed up to the next MAC process. - else if (earliestSequenceNumber == receiveBuffer->getNextExpectedSequenceNumber()) { - passedUp(receiveBuffer, earliestSequenceNumber); - result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); - return result; - } - } - } - else { - if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) { - // A later fragment of a receive-timer-expired MPDU is still a - // received BLOCK_ACK frame, even though the fragment is rejected - // by the tombstone and must be reported to the service for drop - // observability. - agreement->dataFrameReceived(dataHeader); - result.tombstonedFragments.push_back(dataPacket); + // If, after an MPDU is received, the receive buffer is not full, but the sequence number of the complete MSDU or + // A-MSDU in the buffer with the lowest sequence number is equal to the NextExpectedSequenceNumber for + // that Block Ack agreement, then the MPDU shall be passed up to the next MAC process. + else if (earliestSequenceNumber == receiveBuffer->getNextExpectedSequenceNumber()) { + passedUp(receiveBuffer, earliestSequenceNumber); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } - delete dataPacket; } + result.frames = framesToPassUp; return result; } @@ -111,23 +114,14 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq { // The originator shall use the Block Ack starting sequence control to signal the first MPDU in the block for // which an acknowledgment is expected. - SequenceNumberCyclic startingSequenceNumber; - Tid tid = -1; - if (auto basicReq = dynamicPtrCast(blockAckReq)) { - tid = basicReq->getTidInfo(); - startingSequenceNumber = basicReq->getStartingSequenceNumber(); - } - else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { - tid = compressedReq->getTidInfo(); - startingSequenceNumber = compressedReq->getStartingSequenceNumber(); - } - else { + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReq); + if (!blockAckReqDetails) throw cRuntimeError("Multi-Tid BlockAckReq is currently an unimplemented feature"); - } + auto startingSequenceNumber = blockAckReqDetails->startingSequenceNumber; // IEEE Std 802.11-2024, 10.25.6.3-10.25.6.5: adjust WinStartR // from the BAR before generating the response, even without a receive buffer. agreement->getBlockAckRecord()->advanceStartingSequenceNumber(startingSequenceNumber); - auto id = std::make_pair(tid, blockAckReq->getTransmitterAddress()); + auto id = std::make_pair(blockAckReqDetails->tid, blockAckReq->getTransmitterAddress()); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { ReceiveBuffer *receiveBuffer = it->second; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index f3a785bb8d3..29c33010ed4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -59,7 +59,38 @@ void ReceiveBuffer::pruneExpiredFragmentSequences() // data frame, unless the sequence number of the frame is older than the NextExpectedSequenceNumber for that // Block Ack agreement, in which case the frame is discarded because it is either old or a duplicate. // +bool ReceiveBuffer::canInsertFrame(const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) const +{ + auto sequenceNumber = dataHeader->getSequenceNumber(); + auto fragmentNumber = dataHeader->getFragmentNumber(); + if (!(nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize)) + return false; + int retainedLength = length; + for (const auto& entry : buffer) { + if (SequenceNumberCyclic(entry.first) < nextExpectedSequenceNumber) + retainedLength -= entry.second.size(); + } + // IEEE Std 802.11-2024, 9.4.1.13, footnote 26: each fragment + // occupies one receive-buffer slot. + if (retainedLength >= bufferSize) + return false; + auto it = buffer.find(sequenceNumber.get()); + if (it != buffer.end()) { + for (auto fragment : it->second) { + const auto& fragmentHeader = fragment->peekAtFront(); + if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) + return false; + } + } + return true; +} + ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader) +{ + return insertFrameWithResult(dataPacket, dataHeader, nextExpectedSequenceNumber); +} + +ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); @@ -67,36 +98,38 @@ ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet pruneExpiredFragmentSequences(); if (isFragmented && expiredFragmentSequences.find(sequenceNumber.get()) != expiredFragmentSequences.end()) return FrameInsertionResult::REJECTED_EXPIRED; - // The total number of MPDUs in these MSDUs may not - // exceed the reorder buffer size in the receiver. - if (length < bufferSize && nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize) { - auto it = buffer.find(sequenceNumber.get()); - if (it != buffer.end()) { - auto& fragments = it->second; - // TODO efficiency - for (auto fragment : fragments) { - const auto& fragmentHeader = fragment->peekAtFront(); - if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) - return FrameInsertionResult::REJECTED; - } - fragments.push_back(dataPacket); - } - else { - buffer[sequenceNumber.get()].push_back(dataPacket); - bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; + if (!canInsertFrame(dataHeader, nextExpectedSequenceNumber)) + return FrameInsertionResult::REJECTED; + auto it = buffer.find(sequenceNumber.get()); + if (it != buffer.end()) { + auto& fragments = it->second; + // TODO efficiency + for (auto fragment : fragments) { + const auto& fragmentHeader = fragment->peekAtFront(); + if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) + return FrameInsertionResult::REJECTED; } - // The total number of frames that can be sent depends on the total - // number of MPDUs in all the outstanding MSDUs. - length++; - // Once an entry has received a fragmented MPDU, keep that identity - // tied to the generation even if a later malformed or - // unfragmented-shaped header is accepted into the same sequence slot. - auto& bufferEntry = bufferEntries[sequenceNumber.get()]; - bufferEntry.hasFragmentedIdentity |= isFragmented; - bufferEntry.receiveLifetimeActive = bufferEntry.hasFragmentedIdentity && !isComplete(buffer[sequenceNumber.get()]); - return FrameInsertionResult::INSERTED; + fragments.push_back(dataPacket); + } + else { + buffer[sequenceNumber.get()].push_back(dataPacket); + bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; } - return FrameInsertionResult::REJECTED; + // The total number of frames that can be sent depends on the total + // number of MPDUs in all the outstanding MSDUs. + length++; + // Once an entry has received a fragmented MPDU, keep that identity + // tied to the generation even if a later malformed or + // unfragmented-shaped header is accepted into the same sequence slot. + auto& bufferEntry = bufferEntries[sequenceNumber.get()]; + bufferEntry.hasFragmentedIdentity |= isFragmented; + bufferEntry.receiveLifetimeActive = bufferEntry.hasFragmentedIdentity && !isComplete(buffer[sequenceNumber.get()]); + return FrameInsertionResult::INSERTED; +} + +bool ReceiveBuffer::insertFrame(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) +{ + return insertFrameWithResult(dataPacket, dataHeader, nextExpectedSequenceNumber) == FrameInsertionResult::INSERTED; } void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index 9e2349eff21..bc8c9719e8e 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -48,13 +48,16 @@ class INET_API ReceiveBuffer SequenceNumberCyclic nextExpectedSequenceNumber; void pruneExpiredFragmentSequences(); + bool canInsertFrame(const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) const; public: ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSequenceNumber); virtual ~ReceiveBuffer(); FrameInsertionResult insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader); + FrameInsertionResult insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber); bool insertFrame(Packet *dataPacket, const Ptr& dataHeader) { return insertFrameWithResult(dataPacket, dataHeader) == FrameInsertionResult::INSERTED; } + bool insertFrame(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber); void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); Fragments extractFrames(); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 4b1cb39ff69..a225551b9a6 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -17,6 +17,7 @@ #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" @@ -796,11 +797,11 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); - else if (auto blockAckRequest = dynamicPtrCast(header)) { + else if (auto blockAckReqDetails = getOneTidBlockAckReqDetails(header)) { if (recipientBlockAckAgreementHandler) - recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckRequest, this); + recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckReqDetails->blockAckReq, this); if (recipientBlockAckProcedure) - recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckRequest, recipientAckPolicy, recipientBlockAckAgreementHandler, this); + recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckReqDetails->blockAckReq, recipientAckPolicy, recipientBlockAckAgreementHandler, this); } else if (dynamicPtrCast(header)) EV_WARN << "ACK frame received after timeout, ignoring it.\n"; // drop it, it is an ACK frame that is received after the ACKTimeout diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 3db5e4117ce..0911cb8e0cf 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -10,6 +10,7 @@ #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduDeaggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h" #include "inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h" @@ -249,20 +250,13 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c { Enter_Method("controlFrameReceived"); expireReceiveLifetime(); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReqDetails = getOneTidBlockAckReqDetails(controlHeader)) { BlockAckReordering::ReorderBuffer frames; if (blockAckReordering) { - Tid tid = -1; - if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) - tid = basicBlockAckReq->getTidInfo(); - else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) - tid = compressedBlockAckReq->getTidInfo(); - else - return std::vector(); - MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); + MacAddress originatorAddr = blockAckReqDetails->blockAckReq->getTransmitterAddress(); + RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(blockAckReqDetails->tid, originatorAddr); if (agreement) - frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); + frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReqDetails->blockAckReq); else { scheduleReceiveLifetimeTimer(); return std::vector(); diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index e0bbc85340e..d43376db21e 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -6,12 +6,14 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" @@ -28,6 +30,37 @@ class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure using RecipientBlockAckProcedure::buildBlockAck; }; +class SpyRecipientBlockAckProcedure : public IRecipientBlockAckProcedure +{ + public: + std::vector variants; + std::vector tids; + + virtual void processReceivedBlockAckReq(Packet *packet, const Ptr& blockAckReq, IRecipientQosAckPolicy *ackPolicy, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, IProcedureCallback *callback) override + { + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReq); + ASSERT(blockAckReqDetails.has_value()); + variants.push_back(blockAckReqDetails->variant); + tids.push_back(blockAckReqDetails->tid); + } + + virtual void processTransmittedBlockAck(const Ptr& blockAck) override {} +}; + +class TestHcf : public Hcf +{ + public: + void setRecipientBlockAckProcedure(IRecipientBlockAckProcedure *procedure) + { + recipientBlockAckProcedure = procedure; + } + + void processControlHeader(const Ptr& header) + { + recipientProcessReceivedControlFrame(nullptr, header); + } +}; + class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler { public: @@ -181,6 +214,39 @@ static std::pair> makeReceivedQosFrame(Sequen EV << "Compressed BA encoding and round-trip passed.\n"; } +{ + TestHcf hcf; + auto spy = new SpyRecipientBlockAckProcedure(); + hcf.setRecipientBlockAckProcedure(spy); + auto basicRequest = makeShared(); + basicRequest->setTidInfo(3); + basicRequest->setStartingSequenceNumber(SequenceNumberCyclic(4095)); + auto compressedRequest = makeShared(); + compressedRequest->setTidInfo(5); + compressedRequest->setStartingSequenceNumber(SequenceNumberCyclic(7)); + auto basicDetails = getOneTidBlockAckReqDetails(basicRequest); + auto compressedDetails = getOneTidBlockAckReqDetails(compressedRequest); + ASSERT(basicDetails && basicDetails->variant == OneTidBlockAckReqVariant::BASIC && basicDetails->tid == 3 && basicDetails->startingSequenceNumber == SequenceNumberCyclic(4095)); + ASSERT(compressedDetails && compressedDetails->variant == OneTidBlockAckReqVariant::COMPRESSED && compressedDetails->tid == 5 && compressedDetails->startingSequenceNumber == SequenceNumberCyclic(7)); + auto multiTidRequest = makeShared(); + ASSERT(!getOneTidBlockAckReqDetails(multiTidRequest)); + ASSERT(!getOneTidBlockAckReqDetails(makeShared())); + hcf.processControlHeader(basicRequest); + hcf.processControlHeader(compressedRequest); + ASSERT(spy->variants == std::vector({ OneTidBlockAckReqVariant::BASIC, OneTidBlockAckReqVariant::COMPRESSED })); + ASSERT(spy->tids == std::vector({ 3, 5 })); + bool multiTidRejectedAsUnknown = false; + try { + hcf.processControlHeader(multiTidRequest); + } + catch (const cRuntimeError& error) { + multiTidRejectedAsUnknown = std::string(error.what()).find("Unknown control frame") != std::string::npos; + } + ASSERT(multiTidRejectedAsUnknown); + ASSERT(spy->variants.size() == 2); + EV << "HCF accepts one-TID BAR variants and rejects Multi-TID as unknown.\n"; +} + { TestRecipientBlockAckProcedure procedure; auto request = makeCompressedBlockAckReq(); @@ -265,6 +331,8 @@ static std::pair> makeReceivedQosFrame(Sequen record.advanceStartingSequenceNumber(SequenceNumberCyclic(104)); ASSERT(!record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(105), 0)); + record.dataFrameReceived(received102.second, 64); + ASSERT(!record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); delete received102.first; delete received105.first; EV << "Receive-window advancement erases old state and retains overlap.\n"; @@ -357,6 +425,56 @@ static std::pair> makeReceivedQosFrame(Sequen EV << "Future MPDU returns displaced and consecutive complete MSDUs without losing gaps.\n"; } +{ + // IEEE Std 802.11-2024, 9.4.1.13 footnote 26 and 10.25.6.6.2.1(b): + // fragments consume individual slots, and a rejected future MPDU must not move WinStartB. + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(1), 2, SIMTIME_ZERO); + auto incomplete2Fragment0 = makeReceivedQosFrame(2, 0, true); + auto incomplete2Fragment1 = makeReceivedQosFrame(2, 1, true); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete2Fragment0.first, incomplete2Fragment0.second).empty()); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete2Fragment1.first, incomplete2Fragment1.second).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getLength() == 2); + ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); + ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); + + auto received3 = makeReceivedQosFrame(3); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received3.first, received3.second).empty()); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(receiveBuffer->getLength() == 2); + ASSERT(receiveBuffer->getBuffer().size() == 1); + ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); + ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); + ASSERT(receiveBuffer->getBuffer().find(3) == receiveBuffer->getBuffer().end()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(2)); + ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(3))); + EV << "Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(4095), 2, SIMTIME_ZERO); + auto incomplete4095 = makeReceivedQosFrame(4095, 0, true); + auto incomplete0 = makeReceivedQosFrame(0, 0, true); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete4095.first, incomplete4095.second).empty()); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete0.first, incomplete0.second).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getLength() == 2); + + auto received1 = makeReceivedQosFrame(1); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received1.first, received1.second).empty()); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(receiveBuffer->getLength() == 2); + ASSERT(receiveBuffer->getBuffer().find(4095) == receiveBuffer->getBuffer().end()); + ASSERT(receiveBuffer->getBuffer().at(0).size() == 1 && receiveBuffer->getBuffer().at(0)[0] == incomplete0.first); + ASSERT(receiveBuffer->getBuffer().at(1).size() == 1 && receiveBuffer->getBuffer().at(1)[0] == received1.first); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(0)); + EV << "Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB.\n"; +} + { MacAddress originatorAddress("11:22:33:44:55:66"); TestRecipientBlockAckProcedure procedure; @@ -369,6 +487,15 @@ static std::pair> makeReceivedQosFrame(Sequen request->setStartingSequenceNumber(SequenceNumberCyclic(1)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); ASSERT(response->getBlockAckBitmap().getBit(63)); + auto basicRequest = makeShared(); + basicRequest->setTidInfo(5); + basicRequest->setStartingSequenceNumber(SequenceNumberCyclic(0)); + auto basicResponse = dynamicPtrCast(procedure.buildBlockAck(basicRequest, &agreement)); + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(basicResponse->getBlockAckBitmap(0).getBit(fragmentNumber)); + for (int sequenceIndex = 1; sequenceIndex < 64; sequenceIndex++) + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(!basicResponse->getBlockAckBitmap(sequenceIndex).getBit(fragmentNumber)); EV << "Normal-Ack data advances and updates the Block Ack scoreboard.\n"; } @@ -566,9 +693,9 @@ static std::pair> makeReceivedQosFrame(Sequen request->setBlockAckPolicy(true); return request; }; - handler.createAgreement(makeAddbaRequest(listedPeer), &localDisabledPolicy); - handler.createAgreement(makeAddbaRequest(unlistedPeer), &localEnabledPolicy); - handler.createAgreement(makeAddbaRequest(positivePeer), &localEnabledPolicy); + handler.createAgreement(makeAddbaRequest(listedPeer), 1, &localDisabledPolicy); + handler.createAgreement(makeAddbaRequest(unlistedPeer), 2, &localEnabledPolicy); + handler.createAgreement(makeAddbaRequest(positivePeer), 3, &localEnabledPolicy); ASSERT(!handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); ASSERT(!handler.getAgreement(unlistedPeer, 5)->getIsCompressedBlockAckSupported()); ASSERT(handler.getAgreement(positivePeer, 5)->getIsCompressedBlockAckSupported()); @@ -585,19 +712,19 @@ static std::pair> makeReceivedQosFrame(Sequen { MacAddress receiverAddress("10:20:30:40:50:60"); - OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false, 0, 0); immediateAgreement.setIsAddbaResponseReceived(true); std::vector outstandingFrames { makeOutstandingQosFrame(receiverAddress, 5) }; ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr)); immediateAgreement.setIsCompressedBlockAckSupported(true); - OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true); + OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true, 0, 0); delayedAgreement.setIsAddbaResponseReceived(true); delayedAgreement.setIsCompressedBlockAckSupported(true); ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement)); ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); MacAddress legacyReceiverAddress("10:20:30:40:50:61"); - OriginatorBlockAckAgreement legacyAgreement(legacyReceiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + OriginatorBlockAckAgreement legacyAgreement(legacyReceiverAddress, 5, SequenceNumberCyclic(100), 64, false, false, 0, 0); legacyAgreement.setIsAddbaResponseReceived(true); std::vector legacyOutstandingFrames { makeOutstandingQosFrame(legacyReceiverAddress, 5) }; ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(legacyOutstandingFrames, &legacyAgreement)); @@ -645,7 +772,7 @@ static std::pair> makeReceivedQosFrame(Sequen TestOriginatorBlockAckAgreementHandler handler; TestOriginatorBlockAckAgreementPolicy policy; policy.configureCompressedBlockAckCapability(false, {}); - OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false); + OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false, 0, 0); auto delayedResponse = makeShared(); delayedResponse->setBlockAckPolicy(false); delayedResponse->setBufferSize(64); @@ -685,6 +812,7 @@ EV << ".\n"; %contains: stdout Compressed BAR encoding and round-trip passed. Compressed BA encoding and round-trip passed. +HCF accepts one-TID BAR variants and rejects Multi-TID as unknown. Missing recipient state produces a null compressed BA. Empty recipient record produces a null compressed BA. Empty Basic BA distinguishes old and current-window MPDUs. @@ -694,6 +822,8 @@ Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. SN64 advances a size-64 scoreboard and remains buffered across a gap. Future MPDU returns displaced and consecutive complete MSDUs without losing gaps. +Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard. +Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB. Normal-Ack data advances and updates the Block Ack scoreboard. Upward delivery advances only the reorder cursor, not WinStartR. Data-window slide preserves cyclic 4095-to-0 delivery order. From 8ab47d7412411c18438cdc0520a8e53fb1b6fca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 13:37:51 +0200 Subject: [PATCH 65/91] fix(ieee80211): record frames at Block Ack window start Accept WinStartR in the receiver Block Ack bookkeeping and add focused Basic and Compressed Block Ack coverage for the boundary. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 5 ++- tests/unit/Ieee80211BlockAckRecord_1.test | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/unit/Ieee80211BlockAckRecord_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index e212ed19537..54d478446b1 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -24,8 +24,9 @@ void BlockAckRecord::dataFrameReceived(const Ptr& hea SequenceNumberCyclic sequenceNumber = header->getSequenceNumber(); FragmentNumber fragmentNumber = header->getFragmentNumber(); // IEEE Std 802.11-2024, 10.25.6.3(b), case 3: an old related MPDU - // does not change the Block Ack record. - if (!(startingSequenceNumber <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048)) + // does not change the Block Ack record. The cyclic comparison below already + // restricts the accepted range to [WinStartR, WinStartR + 2047]. + if (!(startingSequenceNumber <= sequenceNumber)) return; // Cases 1 and 2: record an in-window MPDU, or advance WinStartR // before recording an MPDU beyond WinEndR (also see 10.25.6.4(c)). diff --git a/tests/unit/Ieee80211BlockAckRecord_1.test b/tests/unit/Ieee80211BlockAckRecord_1.test new file mode 100644 index 00000000000..99b9e16a7c4 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckRecord_1.test @@ -0,0 +1,40 @@ +%description: +Validate that a Block Ack record accepts the MPDU at WinStartR and reports Basic fragment and Compressed state. +IEEE Std 802.11-2024, 10.25.6.3(b), case 1. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +static Ptr makeReceivedQosHeader(SequenceNumber sequenceNumber) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setTid(5); + header->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + header->setFragmentNumber(0); + return header; +} + +%activity: +{ + BlockAckRecord record(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(101)); + record.dataFrameReceived(makeReceivedQosHeader(101), 64); + record.dataFrameReceived(makeReceivedQosHeader(103), 64); + ASSERT(record.getAckState(SequenceNumberCyclic(101), 0)); + ASSERT(!record.getAckState(SequenceNumberCyclic(102), 0)); + ASSERT(record.getAckState(SequenceNumberCyclic(103), 0)); + ASSERT(record.getCompressedAckState(SequenceNumberCyclic(101))); + ASSERT(!record.getCompressedAckState(SequenceNumberCyclic(102))); + ASSERT(record.getCompressedAckState(SequenceNumberCyclic(103))); + EV << "IEEE Std 802.11-2024 10.25.6.3(b) WinStartR boundary is recorded in Basic and Compressed Block Ack state.\n"; +} + +EV << ".\n"; + +%contains: stdout +IEEE Std 802.11-2024 10.25.6.3(b) WinStartR boundary is recorded in Basic and Compressed Block Ack state. +. From 11b2532ebec89c7fad673619b2e0ddc7585e9af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 01:38:06 +0200 Subject: [PATCH 66/91] ieee80211: preserve Block Ack transaction state Scope Basic and Compressed BAR status transitions by receiver, and match incoming Block Ack responses against the active request before completing the receive step or changing recovery state. Unrelated responses leave the wait and timer active. Update the recipient scoreboard after successful buffer admission so capacity-rejected MPDUs remain unacknowledged and failed window advances preserve both cursors. Allow valid negative intermediate values in cyclic sequence arithmetic for wraparound admission. Add multi-peer, response-correlation, buffer-capacity, and sequence-wrap regressions. Correct the rejected-frame test expectation to require an unchanged scoreboard. Validation: debug build, four focused unit tests, two module tests, architecture checks, and independent review passed. Both selected legacy fingerprint mismatches reproduce identically at the unchanged parent; recorded fingerprint baselines remain untouched. --- .../mac/blockack/OneTidBlockAckReqVariant.h | 16 + .../blockackreordering/BlockAckReordering.cc | 13 +- .../ieee80211/mac/common/Ieee80211Defs.h | 2 +- .../mac/contract/IFrameSequenceHandler.h | 4 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 16 +- .../mac/framesequence/FrameSequenceHandler.cc | 26 +- .../mac/framesequence/FrameSequenceHandler.h | 2 +- .../framesequence/PrimitiveFrameSequences.cc | 10 +- .../ieee80211/mac/originator/QosAckHandler.cc | 7 +- ...eee80211BlockAckResponseCorrelation_1.test | 308 ++++++++++++++++++ tests/unit/Ieee80211AddbaTransaction_1.test | 67 +++- tests/unit/Ieee80211CompressedBlockAck_1.test | 27 +- 12 files changed, 476 insertions(+), 22 deletions(-) create mode 100644 tests/module/Ieee80211BlockAckResponseCorrelation_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h index 0d158b365cf..cc340c652a7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -40,6 +40,22 @@ inline std::optional getOneTidBlockAckReqDetails(const return std::nullopt; } +// IEEE Std 802.11-2024, 9.3.1.7, 9.3.1.8, and 10.25.5: correlate the BAR RA +// with the BA TA, TID, and selected BlockAck variant before accepting a response. +inline bool isMatchingOneTidBlockAckResponse(const OneTidBlockAckReqDetails& blockAckReqDetails, const Ptr& blockAck) +{ + if (blockAckReqDetails.blockAckReq->getReceiverAddress() != blockAck->getTransmitterAddress()) + return false; + if (blockAckReqDetails.variant == OneTidBlockAckReqVariant::BASIC) { + auto basicBlockAck = dynamicPtrCast(blockAck); + return basicBlockAck != nullptr && basicBlockAck->getTidInfo() == blockAckReqDetails.tid; + } + else { + auto compressedBlockAck = dynamicPtrCast(blockAck); + return compressedBlockAck != nullptr && compressedBlockAck->getTidInfo() == blockAckReqDetails.tid; + } +} + } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 2ec88f1ccd0..145604677c1 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -37,9 +37,6 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived auto sequenceNumber = dataHeader->getSequenceNumber(); auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; - // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: update the - // scoreboard for every related Data frame, independently of reorder storage. - agreement->dataFrameReceived(dataHeader); SequenceNumberCyclic newStartingSequenceNumber; if (advancesWindow) { // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): store the future MPDU @@ -55,12 +52,20 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived receiveBuffer->insertFrameWithResult(dataPacket, dataHeader, newStartingSequenceNumber) : receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); if (insertionResult != ReceiveBuffer::FrameInsertionResult::INSERTED) { - if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) + if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) { + // Preserve the receive-lifetime tombstone policy for late Block Ack fragments. + if (dataHeader->getAckPolicy() == BLOCK_ACK) + agreement->dataFrameReceived(dataHeader); result.tombstonedFragments.push_back(dataPacket); + } else delete dataPacket; return result; } + // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.6.2.1: acknowledge an + // MPDU after it has been admitted to the receive buffer, so a capacity- + // rejected MPDU cannot be reported as received. + agreement->dataFrameReceived(dataHeader); if (advancesWindow) { framesToPassUp = collectCompletePrecedingMpdus(receiveBuffer, newStartingSequenceNumber); for (const auto& entry : framesToPassUp) diff --git a/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h b/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h index 3263f1cd40b..a3019267ecd 100644 --- a/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h +++ b/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h @@ -23,7 +23,7 @@ struct SequenceNumberCyclic SequenceNumber value; private: - SequenceNumber modulo4096(SequenceNumber value) const { ASSERT(value != -1); return (value % 4096 + 4096) % 4096; } // always returns positive result + SequenceNumber modulo4096(SequenceNumber value) const { return (value % 4096 + 4096) % 4096; } // always returns positive result SequenceNumber distance4096(SequenceNumber other) const { ASSERT(0 <= other && other < 4096); return (value - other + 4096) % 4096; } public: diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index a71e80607c9..4b02b5af006 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -42,7 +42,9 @@ class INET_API IFrameSequenceHandler virtual const FrameSequenceContext *getContext() const = 0; virtual const IFrameSequence *getFrameSequence() const = 0; virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, ICallback *callback) = 0; - virtual void processResponse(Packet *frame) = 0; + // Returns true when the handler consumes the frame; false leaves ownership + // with the caller because the active receive step remains unchanged. + virtual bool processResponse(Packet *frame) = 0; virtual void transmissionComplete() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index a225551b9a6..17b61a887ab 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -613,12 +613,19 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& if (edcaf && frameSequenceHandler->isSequenceRunning()) { // TODO always call processResponse? if ((!isForUs(header) && !startRxTimer->isScheduled()) || isForUs(header)) { - frameSequenceHandler->processResponse(packet); // Only cancel RxTimer when the current running sequence has been handled by frameSequenceHandler->processResponse(). // If the received frame is not for us, we are still waiting to receive our ACK. In that case, don't cancel the timer. // Otherwise, current frame sequence stucks in RX step and runs longer than intendeed, preventing sequence from // another access category (AC) to start running (RuntimeError("Channel access granted while a frame sequence is running")). - cancelEvent(startRxTimer); + if (frameSequenceHandler->processResponse(packet)) + cancelEvent(startRxTimer); + else { + EV_INFO << "Ignoring a response that does not match the active frame sequence step." << endl; + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } } else { EV_INFO << "This frame is not for us" << std::endl; @@ -1241,6 +1248,11 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { EV_INFO << blockAck->getClassName() << " has arrived" << std::endl; + auto blockAckReqDetails = getOneTidBlockAckReqDetails(lastTransmittedHeader); + if (!blockAckReqDetails || !isMatchingOneTidBlockAckResponse(*blockAckReqDetails, blockAck)) { + EV_INFO << "Ignoring BlockAck that does not match the transmitted BlockAckReq.\n"; + return; + } Tid tid = -1; MacAddress transmitterAddress = blockAck->getTransmitterAddress(); if (auto basicBlockAck = dynamicPtrCast(blockAck)) diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index a70939b288b..ff49f6ef264 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -8,12 +8,30 @@ #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h" #include "inet/common/INETUtils.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" namespace inet { namespace ieee80211 { +static bool isUnexpectedBlockAckResponse(const FrameSequenceContext *context, Packet *frame) +{ + if (context == nullptr || frame == nullptr) + return false; + auto receiveStep = dynamic_cast(context->getLastStep()); + auto transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (receiveStep == nullptr || transmitStep == nullptr || transmitStep->getFrameToTransmit() == nullptr) + return false; + auto receivedHeader = frame->peekAtFront(); + auto blockAck = dynamicPtrCast(receivedHeader); + if (blockAck == nullptr) + return false; + auto blockAckReqHeader = transmitStep->getFrameToTransmit()->peekAtFront(); + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReqHeader); + return blockAckReqDetails && !isMatchingOneTidBlockAckResponse(*blockAckReqDetails, blockAck); +} + void FrameSequenceHandler::handleStartRxTimeout() { auto lastStep = context->getLastStep(); @@ -28,24 +46,26 @@ void FrameSequenceHandler::handleStartRxTimeout() } } -void FrameSequenceHandler::processResponse(Packet *frame) +bool FrameSequenceHandler::processResponse(Packet *frame) { ASSERT(callback != nullptr); if (frameSequenceCancellationRequested) { delete frame; abortFrameSequence(); - return; + return true; } auto lastStep = context->getLastStep(); switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { + if (isUnexpectedBlockAckResponse(context, frame)) + return false; // TODO check if not for us and abort auto receiveStep = check_and_cast(context->getLastStep()); receiveStep->setFrameToReceive(frame); finishFrameSequenceStep(); if (isSequenceRunning()) startFrameSequenceStep(); - break; + return true; } case IFrameSequenceStep::Type::TRANSMIT: throw cRuntimeError("Received frame while current step is transmit"); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index 4dca5f0c99d..21a4eec7be1 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -32,7 +32,7 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual const FrameSequenceContext *getContext() const override { return context; } virtual const IFrameSequence *getFrameSequence() const override { return frameSequence; } virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, IFrameSequenceHandler::ICallback *callback) override; - virtual void processResponse(Packet *frame) override; + virtual bool processResponse(Packet *frame) override; virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 69289dc51fb..d62dffe18a0 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" + namespace inet { namespace ieee80211 { @@ -431,7 +433,13 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) step++; auto receivedPacket = receiveStep->getReceivedFrame(); const auto& receivedHeader = receivedPacket->peekAtFront(); - return context->isForUs(receivedHeader) && receivedHeader->getType() == ST_BLOCKACK; + auto transmitStep = dynamic_cast(context->getStep(firstStep)); + if (transmitStep == nullptr || transmitStep->getFrameToTransmit() == nullptr) + return false; + auto blockAckReqHeader = transmitStep->getFrameToTransmit()->peekAtFront(); + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReqHeader); + auto blockAck = dynamicPtrCast(receivedHeader); + return context->isForUs(receivedHeader) && receivedHeader->getType() == ST_BLOCKACK && blockAckReqDetails && blockAck != nullptr && isMatchingOneTidBlockAckResponse(*blockAckReqDetails, blockAck); } default: throw cRuntimeError("Unknown step"); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index b808f44fc96..e33d1fffbf1 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -213,20 +213,19 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const Ptr& blockAckReq) { for (auto& ackStatus : ackStatuses) { - if (ackStatus.first.first != blockAckReq->getReceiverAddress()) - continue; + auto receiverAddress = ackStatus.first.first; auto tid = ackStatus.first.second.first; auto seqCtrlField = ackStatus.first.second.second; auto& status = ackStatus.second; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { - if (basicBlockAckReq->getTidInfo() == tid) { + if (receiverAddress == blockAckReq->getReceiverAddress() && basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum) status = Status::WAITING_FOR_BLOCK_ACK; } } else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { - if (compressedBlockAckReq->getTidInfo() == tid) { + if (receiverAddress == blockAckReq->getReceiverAddress() && compressedBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = compressedBlockAckReq->getStartingSequenceNumber(); if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum && seqCtrlField.getFragmentNumber() == 0) // TODO ASSERT(seqCtrlField.second == 0)? status = Status::WAITING_FOR_BLOCK_ACK; diff --git a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test new file mode 100644 index 00000000000..cd7367a61b8 --- /dev/null +++ b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test @@ -0,0 +1,308 @@ +%description: +Exercise the live HCF lower-frame entry while a Block Ack response step is +waiting. A Block Ack for another active peer, for the wrong TID, or with the +wrong Basic/Compressed variant must leave the current response wait and its +timeout armed. The matching response then completes that same frame sequence. + +%file: TestIeee80211BlockAckResponseCorrelation.cc + +#include +#include + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" + +namespace inet { +namespace ieee80211 { + +static const MacAddress LOCAL_ADDRESS("02:00:00:00:00:01"); +static const MacAddress PEER_ONE("02:00:00:00:00:11"); +static const MacAddress PEER_TWO("02:00:00:00:00:12"); +static constexpr Tid CURRENT_TID = 6; +static constexpr SequenceNumber CURRENT_SSN = 700; + +class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler +{ + public: + void addEstablishedAgreement(const MacAddress& receiverAddress, Tid tid) + { + auto transactionId = receiverAddress == PEER_ONE ? 1 : 2; + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(CURRENT_SSN), 64, true, false, 1, transactionId); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(0); + agreement->calculateExpirationTime(); + blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; + } +}; + +class TestOriginatorQosAckPolicy : public IOriginatorQoSAckPolicy +{ + public: + bool compressed = true; + + virtual bool isAckNeeded(const Ptr&) const override { return true; } + virtual AckPolicy computeAckPolicy(Packet *, const Ptr&, OriginatorBlockAckAgreement *) const override { return BLOCK_ACK; } + virtual bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return true; } + virtual bool isBlockAckPolicyEligibleFrame(Packet *, const Ptr&) const override { return true; } + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override + { + return std::make_tuple(PEER_ONE, SequenceNumberCyclic(CURRENT_SSN), CURRENT_TID); + } + virtual bool isCompressedBlockAckReq(const std::vector&, OriginatorBlockAckAgreement *) const override { return compressed; } + virtual simtime_t getAckTimeout(Packet *, const Ptr&) const override { return SimTime(1, SIMTIME_MS); } + virtual simtime_t getBlockAckTimeout(Packet *, const Ptr&) const override { return SimTime(1, SIMTIME_MS); } +}; + +class TestInProgressFrames : public InProgressFrames +{ + public: + virtual std::vector getOutstandingFrames() override { return {}; } +}; + +class TestEdcaf : public Edcaf +{ +}; + +class TestEdca : public Edca +{ + protected: + TestEdcaf *channelOwner = nullptr; + + public: + TestEdca(TestEdcaf *channelOwner) : channelOwner(channelOwner) {} + virtual Edcaf *getChannelOwner() override { return channelOwner; } +}; + +class TestTxopProcedure : public TxopProcedure +{ + public: + virtual simtime_t getRemaining() const override { return SimTime(1, SIMTIME_MS); } +}; + +class TestFrameSequenceContext : public FrameSequenceContext +{ + public: + TestFrameSequenceContext(InProgressFrames *inProgressFrames, QoSContext *qosContext) : + FrameSequenceContext(LOCAL_ADDRESS, nullptr, inProgressFrames, nullptr, nullptr, nullptr, qosContext) + { + } + + virtual simtime_t getIfs() const override { return SIMTIME_ZERO; } +}; + +class TestHcfBlockAckResponseCorrelation : public Hcf +{ + protected: + TestEdcaf channelOwner; + TestEdca testEdca; + MacAddress localAddress = LOCAL_ADDRESS; + + virtual int numInitStages() const override { return 1; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) { + startRxTimer = new cMessage("startRxTimeout"); + inactivityTimer = new cMessage("inactivityTimeout"); + addbaResponseTimer = new cMessage("addbaResponseTimeout"); + frameSequenceHandler = new FrameSequenceHandler(); + edca = &testEdca; + } + } + + virtual bool isForUs(const Ptr& header) const override + { + return header->getReceiverAddress() == localAddress; + } + + virtual void transmitFrame(Packet *, simtime_t) override + { + transmittedFrames++; + // The synthetic test frame is handed to the live FrameSequenceHandler + // at the same boundary where Tx would report PHY-TXEND.confirm. + frameSequenceHandler->transmissionComplete(); + } + + virtual void originatorProcessTransmittedFrame(Packet *) override {} + virtual void originatorProcessReceivedFrame(Packet *, Packet *) override { acceptedResponses++; } + virtual void originatorProcessFailedFrame(Packet *) override {} + virtual void frameSequenceFinished() override { finishedSequences++; } + + virtual void scheduleStartRxTimer(simtime_t timeout) override + { + scheduleAfter(timeout, startRxTimer); + } + + public: + int transmittedFrames = 0; + int acceptedResponses = 0; + int finishedSequences = 0; + + TestHcfBlockAckResponseCorrelation() : testEdca(&channelOwner) {} + + void startBlockAckResponseWait(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, + IOriginatorBlockAckAgreementHandler *agreementHandler, InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) + { + Enter_Method("startBlockAckResponseWait"); + auto qosContext = new QoSContext(ackPolicy, blockAckProcedure, agreementHandler, txopProcedure); + auto context = new TestFrameSequenceContext(inProgressFrames, qosContext); + frameSequenceHandler->startFrameSequence(new BlockAckReqBlockAckFs(), context, this); + } + + void injectBlockAck(const MacAddress& transmitterAddress, Tid tid, bool compressed) + { + Enter_Method("injectBlockAck"); + Ptr blockAck; + if (compressed) { + auto compressedBlockAck = makeShared(); + compressedBlockAck->setTidInfo(tid); + compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + blockAck = compressedBlockAck; + } + else { + auto basicBlockAck = makeShared(); + basicBlockAck->setTidInfo(tid); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + blockAck = basicBlockAck; + } + blockAck->setReceiverAddress(localAddress); + blockAck->setTransmitterAddress(transmitterAddress); + auto packet = new Packet("injectedBlockAck", blockAck); + Hcf::processLowerFrame(packet, blockAck); + } + + bool isResponseWaitActive() const { return frameSequenceHandler->isSequenceRunning(); } + bool isResponseTimerScheduled() const { return startRxTimer->isScheduled(); } +}; + +Define_Module(TestHcfBlockAckResponseCorrelation); + +class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule +{ + public: + Ieee80211BlockAckResponseCorrelationTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + auto hcf = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf")); + for (bool compressed : { false, true }) { + TestOriginatorBlockAckAgreementHandler agreementHandler; + agreementHandler.addEstablishedAgreement(PEER_ONE, CURRENT_TID); + agreementHandler.addEstablishedAgreement(PEER_TWO, CURRENT_TID); + ASSERT(agreementHandler.getActiveAgreement(PEER_ONE, CURRENT_TID) != nullptr); + ASSERT(agreementHandler.getActiveAgreement(PEER_TWO, CURRENT_TID) != nullptr); + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.compressed = compressed; + OriginatorBlockAckProcedure blockAckProcedure; + TestInProgressFrames inProgressFrames; + TestTxopProcedure txopProcedure; + + auto expectedTransmittedFrames = hcf->transmittedFrames + 1; + auto expectedAcceptedResponses = hcf->acceptedResponses + 1; + auto expectedFinishedSequences = hcf->finishedSequences + 1; + hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); + ASSERT(hcf->transmittedFrames == expectedTransmittedFrames); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + + // All three frames are addressed to this STA, so HCF must inspect + // the active BAR request before letting any one end the wait. + hcf->injectBlockAck(PEER_TWO, CURRENT_TID, compressed); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectBlockAck(PEER_ONE, CURRENT_TID + 1, compressed); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, !compressed); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, compressed); + ASSERT(!hcf->isResponseWaitActive()); + ASSERT(!hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->finishedSequences == expectedFinishedSequences); + } + std::cout << "HCF keeps the active Block Ack response wait scoped by peer, TID, and variant.\n"; + endSimulation(); + } +}; + +Define_Module(Ieee80211BlockAckResponseCorrelationTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module TestHcfBlockAckResponseCorrelation extends Hcf +{ + parameters: + @class(::inet::ieee80211::TestHcfBlockAckResponseCorrelation); +} + +simple Ieee80211BlockAckResponseCorrelationTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211BlockAckResponseCorrelationTest); +} + +network Ieee80211BlockAckResponseCorrelationTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint { + parameters: + wlan[*].mac.qosStation = true; + wlan[*].mac.hcf.typename = "TestHcfBlockAckResponseCorrelation"; + wlan[*].mgmt.beaconInterval = 10s; + } + test: Ieee80211BlockAckResponseCorrelationTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211BlockAckResponseCorrelationTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +HCF keeps the active Block Ack response wait scoped by peer, TID, and variant. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 0063793843d..aad0aaffb02 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -911,7 +911,7 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler running = true; numStartedSequences++; } - virtual void processResponse(Packet *) override {} + virtual bool processResponse(Packet *) override { return true; } virtual void transmissionComplete() override {} virtual bool isSequenceRunning() override { return running; } virtual void handleStartRxTimeout() override {} @@ -6117,6 +6117,71 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) delete frame; } +// A transmitted BAR selects one originator agreement by receiver and TID. +// IEEE Std 802.11-2024, 10.25.6.1: identical TIDs and overlapping sequence +// ranges at different peers remain independent Block Ack state machines. +for (bool compressed : { false, true }) { + TestQosAckHandler ackHandler; + auto peer1DataHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(700)); + auto peer2DataHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(700)); + peer1DataHeader->setAckPolicy(BLOCK_ACK); + peer2DataHeader->setAckPolicy(BLOCK_ACK); + auto peer1Frame = new Packet("peer1OverlappingData", peer1DataHeader); + auto peer2Frame = new Packet("peer2OverlappingData", peer2DataHeader); + auto peer1Header = peer1Frame->peekAtFront(); + auto peer2Header = peer2Frame->peekAtFront(); + ackHandler.frameGotInProgress(peer1Header); + ackHandler.processTransmittedDataOrMgmtFrame(peer1Header); + ackHandler.frameGotInProgress(peer2Header); + ackHandler.processTransmittedDataOrMgmtFrame(peer2Header); + + Ptr peer1BasicBar; + Ptr peer2BasicBar; + Ptr peer1CompressedBar; + Ptr peer2CompressedBar; + if (compressed) { + peer1CompressedBar = makeShared(); + peer2CompressedBar = makeShared(); + peer1CompressedBar->setReceiverAddress(peer1); + peer1CompressedBar->setTidInfo(6); + peer1CompressedBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + peer2CompressedBar->setReceiverAddress(peer2); + peer2CompressedBar->setTidInfo(6); + peer2CompressedBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + } + else { + peer1BasicBar = makeShared(); + peer2BasicBar = makeShared(); + peer1BasicBar->setReceiverAddress(peer1); + peer1BasicBar->setTidInfo(6); + peer1BasicBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + peer2BasicBar->setReceiverAddress(peer2); + peer2BasicBar->setTidInfo(6); + peer2BasicBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + } + Ptr peer1Bar; + Ptr peer2Bar; + if (compressed) { + peer1Bar = peer1CompressedBar; + peer2Bar = peer2CompressedBar; + } + else { + peer1Bar = peer1BasicBar; + peer2Bar = peer2BasicBar; + } + + ackHandler.processTransmittedBlockAckReq(peer1Bar); + ASSERT(ackHandler.getQoSDataAckStatus(peer1Header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(ackHandler.getQoSDataAckStatus(peer2Header) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + + // The second peer remains eligible until its own BAR is transmitted. + ackHandler.processTransmittedBlockAckReq(peer2Bar); + ASSERT(ackHandler.getQoSDataAckStatus(peer1Header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(ackHandler.getQoSDataAckStatus(peer2Header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + delete peer1Frame; + delete peer2Frame; +} + // Any terminal originator agreement removal releases outstanding BA-policy // frames as well. Otherwise active-agreement BAR filtering would leave those // frames permanently ineligible after peer or local DELBA teardown. diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index d43376db21e..411bf47b060 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -175,6 +175,11 @@ static std::pair> makeReceivedQosFrame(Sequen } %activity: +ASSERT(SequenceNumberCyclic(0) - 1 == SequenceNumberCyclic(4095)); +ASSERT(SequenceNumberCyclic(1) - 2 + 1 == SequenceNumberCyclic(0)); +ASSERT(SequenceNumberCyclic(4095) + 1 == SequenceNumberCyclic(0)); +EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediate values.\n"; + { auto request = makeCompressedBlockAckReq(); Packet packet("compressedBar", request); @@ -440,6 +445,15 @@ static std::pair> makeReceivedQosFrame(Sequen ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); + // An in-window MPDU is also rejected when incomplete fragments already + // occupy every receive-buffer slot; it must remain absent from the BA + // scoreboard and leave WinStartB unchanged. + auto received1 = makeReceivedQosFrame(1); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received1.first, received1.second).empty()); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(!agreement.getBlockAckRecord()->getAckState(SequenceNumberCyclic(1), 0)); + ASSERT(!agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(1))); + auto received3 = makeReceivedQosFrame(3); ASSERT(reordering.processReceivedQoSFrame(&agreement, received3.first, received3.second).empty()); ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); @@ -448,9 +462,10 @@ static std::pair> makeReceivedQosFrame(Sequen ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); ASSERT(receiveBuffer->getBuffer().find(3) == receiveBuffer->getBuffer().end()); - ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(2)); - ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(3))); - EV << "Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard.\n"; + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(!agreement.getBlockAckRecord()->getAckState(SequenceNumberCyclic(3), 0)); + ASSERT(!agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(3))); + EV << "Rejected future MPDU preserves a fragment-full reorder window and leaves the scoreboard unchanged.\n"; } { @@ -472,6 +487,9 @@ static std::pair> makeReceivedQosFrame(Sequen ASSERT(receiveBuffer->getBuffer().at(0).size() == 1 && receiveBuffer->getBuffer().at(0)[0] == incomplete0.first); ASSERT(receiveBuffer->getBuffer().at(1).size() == 1 && receiveBuffer->getBuffer().at(1)[0] == received1.first); ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == agreement.getStartingSequenceNumber()); + ASSERT(agreement.getBlockAckRecord()->getAckState(SequenceNumberCyclic(1), 0)); + ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(1))); EV << "Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB.\n"; } @@ -810,6 +828,7 @@ static std::pair> makeReceivedQosFrame(Sequen EV << ".\n"; %contains: stdout +Cyclic sequence arithmetic wraps through zero without invalid intermediate values. Compressed BAR encoding and round-trip passed. Compressed BA encoding and round-trip passed. HCF accepts one-TID BAR variants and rejects Multi-TID as unknown. @@ -822,7 +841,7 @@ Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. SN64 advances a size-64 scoreboard and remains buffered across a gap. Future MPDU returns displaced and consecutive complete MSDUs without losing gaps. -Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard. +Rejected future MPDU preserves a fragment-full reorder window and leaves the scoreboard unchanged. Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB. Normal-Ack data advances and updates the Block Ack scoreboard. Upward delivery advances only the reorder cursor, not WinStartR. From 7bc3a39a5b0676a02efee8529a0293180d0452d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 11:10:06 +0200 Subject: [PATCH 67/91] ieee80211: correlate responses with the active receive step An ACK, CTS, data frame, or unrelated control frame addressed to the station could abort a BAR exchange while it awaited a Block Ack. Reject frames outside the expected response type before transferring ownership or handling cancellation, while retaining peer, TID, and variant matching for Block Ack responses. Have DCF honor rejected responses without cancelling the receive timer. Cover Basic and Compressed BAR waits through matching completion and timeout, including an unrelated frame arriving halfway through the wait. --- .../ieee80211/mac/contract/IFrameSequence.h | 1 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 11 +- .../mac/framesequence/FrameSequenceHandler.cc | 10 +- .../mac/framesequence/FrameSequenceStep.h | 9 +- .../framesequence/PrimitiveFrameSequences.cc | 30 +++- ...eee80211BlockAckResponseCorrelation_1.test | 167 ++++++++++++++++-- 6 files changed, 203 insertions(+), 25 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h index af56920ed87..8678b0d067d 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h @@ -55,6 +55,7 @@ class INET_API IReceiveStep : public IFrameSequenceStep virtual simtime_t getTimeout() = 0; virtual Packet *getReceivedFrame() = 0; virtual void setFrameToReceive(Packet *frame) = 0; + virtual bool isExpectedResponse(Packet *frame, FrameSequenceContext *context) const = 0; }; class INET_API IFrameSequence diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 5416a21a455..65bfef59d83 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -312,7 +312,15 @@ void Dcf::processLowerFrame(Packet *packet, const Ptr& if (frameSequenceHandler->isSequenceRunning()) { // TODO always call processResponses if ((!isForUs(header) && !startRxTimer->isScheduled()) || isForUs(header)) { - frameSequenceHandler->processResponse(packet); + if (frameSequenceHandler->processResponse(packet)) + cancelEvent(startRxTimer); + else { + EV_INFO << "Ignoring a response that does not match the active frame sequence step." << std::endl; + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } } else { EV_INFO << "This frame is not for us" << std::endl; @@ -321,7 +329,6 @@ void Dcf::processLowerFrame(Packet *packet, const Ptr& emit(packetDroppedSignal, packet, &details); delete packet; } - cancelEvent(startRxTimer); } else if (isForUs(header)) recipientProcessReceivedFrame(packet, header); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index ff49f6ef264..20d509f09a2 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -49,17 +49,19 @@ void FrameSequenceHandler::handleStartRxTimeout() bool FrameSequenceHandler::processResponse(Packet *frame) { ASSERT(callback != nullptr); + auto lastStep = context->getLastStep(); + if (lastStep->getType() == IFrameSequenceStep::Type::RECEIVE) { + auto receiveStep = check_and_cast(lastStep); + if (!receiveStep->isExpectedResponse(frame, context) || isUnexpectedBlockAckResponse(context, frame)) + return false; + } if (frameSequenceCancellationRequested) { delete frame; abortFrameSequence(); return true; } - auto lastStep = context->getLastStep(); switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { - if (isUnexpectedBlockAckResponse(context, frame)) - return false; - // TODO check if not for us and abort auto receiveStep = check_and_cast(context->getLastStep()); receiveStep->setFrameToReceive(frame); finishFrameSequenceStep(); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h index bef73d418da..9c329a6fc5a 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h @@ -8,6 +8,8 @@ #ifndef __INET_FRAMESEQUENCESTEP_H #define __INET_FRAMESEQUENCESTEP_H +#include + #include "inet/linklayer/ieee80211/mac/contract/IFrameSequence.h" namespace inet { @@ -56,10 +58,12 @@ class INET_API ReceiveStep : public IReceiveStep Completion completion = Completion::UNDEFINED; simtime_t timeout = -1; Packet *receivedFrame = nullptr; + std::function responseValidator; public: - ReceiveStep(simtime_t timeout = -1) : - timeout(timeout) + ReceiveStep(simtime_t timeout = -1, std::function responseValidator = nullptr) : + timeout(timeout), + responseValidator(responseValidator) {} virtual ~ReceiveStep() { delete receivedFrame; } @@ -68,6 +72,7 @@ class INET_API ReceiveStep : public IReceiveStep virtual simtime_t getTimeout() override { return timeout; } virtual Packet *getReceivedFrame() override { return receivedFrame; } virtual void setFrameToReceive(Packet *frame) override { this->receivedFrame = frame; } + virtual bool isExpectedResponse(Packet *frame, FrameSequenceContext *context) const override { return responseValidator == nullptr || responseValidator(frame, context); } }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index d62dffe18a0..d62158e4e38 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -12,6 +12,22 @@ namespace inet { namespace ieee80211 { +namespace { + +// IEEE Std 802.11-2024, Annex G.4, 10.3.2.9, and 10.25.5: a frame exchange +// accepts the specified response type addressed to this station. +auto expectedResponse(Ieee80211FrameType type) +{ + return [type](Packet *packet, FrameSequenceContext *context) { + if (packet == nullptr || context == nullptr) + return false; + auto header = packet->peekAtFront(); + return context->isForUs(header) && header->getType() == type; + }; +} + +} // namespace + // TODO remove isForUs checks it's already done in framesequencehandler void SelfCtsFs::startSequence(FrameSequenceContext *context, int firstStep) @@ -79,7 +95,7 @@ IFrameSequenceStep *CtsFs::prepareStep(FrameSequenceContext *context) case 0: { auto txStep = check_and_cast(context->getLastStep()); auto rtsPacket = txStep->getFrameToTransmit(); - return new ReceiveStep(context->getCtsTimeout(rtsPacket, rtsPacket->peekAtFront())); + return new ReceiveStep(context->getCtsTimeout(rtsPacket, rtsPacket->peekAtFront()), expectedResponse(ST_CTS)); } case 1: return nullptr; @@ -151,7 +167,7 @@ IFrameSequenceStep *ManagementAckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto mgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, mgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, mgmtHeader), expectedResponse(ST_ACK)); } case 2: return nullptr; @@ -223,7 +239,7 @@ IFrameSequenceStep *AckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader), expectedResponse(ST_ACK)); } case 1: return nullptr; @@ -269,7 +285,7 @@ IFrameSequenceStep *RtsCtsFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto rtsFrame = packet->peekAtFront(); - return new ReceiveStep(context->getCtsTimeout(packet, rtsFrame)); + return new ReceiveStep(context->getCtsTimeout(packet, rtsFrame), expectedResponse(ST_CTS)); } case 2: return nullptr; @@ -313,7 +329,7 @@ IFrameSequenceStep *FragFrameAckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader), expectedResponse(ST_ACK)); } case 2: return nullptr; @@ -357,7 +373,7 @@ IFrameSequenceStep *LastFrameAckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader), expectedResponse(ST_ACK)); } case 2: return nullptr; @@ -413,7 +429,7 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto blockAckReq = packet->peekAtFront(); - return new ReceiveStep(context->getQoSContext()->ackPolicy->getBlockAckTimeout(packet, blockAckReq)); + return new ReceiveStep(context->getQoSContext()->ackPolicy->getBlockAckTimeout(packet, blockAckReq), expectedResponse(ST_BLOCKACK)); } case 2: return nullptr; diff --git a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test index cd7367a61b8..64255a2e979 100644 --- a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test +++ b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test @@ -1,8 +1,10 @@ %description: Exercise the live HCF lower-frame entry while a Block Ack response step is -waiting. A Block Ack for another active peer, for the wrong TID, or with the -wrong Basic/Compressed variant must leave the current response wait and its -timeout armed. The matching response then completes that same frame sequence. +waiting. An ACK, CTS, data frame, or other control frame addressed to this +station, as well as a Block Ack for another active peer, the wrong TID, or the +wrong Basic/Compressed variant, must leave the current response wait and its +timeout armed. The matching response then completes that same frame sequence, +while an unmatched wait ends only when its response timer expires. %file: TestIeee80211BlockAckResponseCorrelation.cc @@ -127,6 +129,8 @@ class TestHcfBlockAckResponseCorrelation : public Hcf return header->getReceiverAddress() == localAddress; } + virtual bool isReceptionInProgress() override { return false; } + virtual void transmitFrame(Packet *, simtime_t) override { transmittedFrames++; @@ -137,8 +141,12 @@ class TestHcfBlockAckResponseCorrelation : public Hcf virtual void originatorProcessTransmittedFrame(Packet *) override {} virtual void originatorProcessReceivedFrame(Packet *, Packet *) override { acceptedResponses++; } - virtual void originatorProcessFailedFrame(Packet *) override {} - virtual void frameSequenceFinished() override { finishedSequences++; } + virtual void originatorProcessFailedFrame(Packet *) override { failedFrames++; } + virtual void frameSequenceFinished() override + { + finishedSequences++; + lastFinishedAt = simTime(); + } virtual void scheduleStartRxTimer(simtime_t timeout) override { @@ -148,7 +156,9 @@ class TestHcfBlockAckResponseCorrelation : public Hcf public: int transmittedFrames = 0; int acceptedResponses = 0; + int failedFrames = 0; int finishedSequences = 0; + simtime_t lastFinishedAt = SIMTIME_ZERO; TestHcfBlockAckResponseCorrelation() : testEdca(&channelOwner) {} @@ -183,8 +193,57 @@ class TestHcfBlockAckResponseCorrelation : public Hcf Hcf::processLowerFrame(packet, blockAck); } + void injectAck() + { + Enter_Method("injectAck"); + auto ack = makeShared(); + ack->setReceiverAddress(localAddress); + auto packet = new Packet("injectedAck", ack); + Hcf::processLowerFrame(packet, ack); + } + + void injectCts() + { + Enter_Method("injectCts"); + auto cts = makeShared(); + cts->setReceiverAddress(localAddress); + auto packet = new Packet("injectedCts", cts); + Hcf::processLowerFrame(packet, cts); + } + + void injectData() + { + Enter_Method("injectData"); + auto data = makeShared(); + data->setReceiverAddress(localAddress); + data->setTransmitterAddress(PEER_ONE); + data->setAddress3(LOCAL_ADDRESS); + data->setSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + auto packet = new Packet("injectedData", data); + Hcf::processLowerFrame(packet, data); + } + + void injectRts() + { + Enter_Method("injectRts"); + auto rts = makeShared(); + rts->setReceiverAddress(localAddress); + rts->setTransmitterAddress(PEER_ONE); + auto packet = new Packet("injectedRts", rts); + Hcf::processLowerFrame(packet, rts); + } + bool isResponseWaitActive() const { return frameSequenceHandler->isSequenceRunning(); } bool isResponseTimerScheduled() const { return startRxTimer->isScheduled(); } + simtime_t getResponseTimerArrival() const { return startRxTimer->getArrivalTime(); } + bool hasReceivedResponse() const + { + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto receiveStep = dynamic_cast(context->getLastStep()); + return receiveStep != nullptr && receiveStep->getReceivedFrame() != nullptr; + } }; Define_Module(TestHcfBlockAckResponseCorrelation); @@ -212,29 +271,73 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule auto expectedTransmittedFrames = hcf->transmittedFrames + 1; auto expectedAcceptedResponses = hcf->acceptedResponses + 1; + auto expectedFailedFrames = hcf->failedFrames; auto expectedFinishedSequences = hcf->finishedSequences + 1; hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); ASSERT(hcf->transmittedFrames == expectedTransmittedFrames); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + const auto responseDeadline = simTime() + SimTime(1, SIMTIME_MS); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + + // Every frame below is addressed to this STA. HCF must leave each + // one outside the active BAR response step and retain its exact + // response deadline. + hcf->injectAck(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectCts(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); - // All three frames are addressed to this STA, so HCF must inspect - // the active BAR request before letting any one end the wait. + hcf->injectData(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectRts(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + // Block Ack frames for the wrong peer, TID, or variant must obey + // the same ownership and timer boundary. hcf->injectBlockAck(PEER_TWO, CURRENT_TID, compressed); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); hcf->injectBlockAck(PEER_ONE, CURRENT_TID + 1, compressed); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); hcf->injectBlockAck(PEER_ONE, CURRENT_TID, !compressed); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); @@ -242,9 +345,53 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(!hcf->isResponseWaitActive()); ASSERT(!hcf->isResponseTimerScheduled()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->failedFrames == expectedFailedFrames); + ASSERT(hcf->finishedSequences == expectedFinishedSequences); + + // A fresh BAR wait with no matching response must stay alive + // through the same unrelated traffic and finish only at timeout. + expectedTransmittedFrames = hcf->transmittedFrames + 1; + expectedAcceptedResponses = hcf->acceptedResponses; + expectedFailedFrames = hcf->failedFrames + 1; + expectedFinishedSequences = hcf->finishedSequences + 1; + hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); + ASSERT(hcf->transmittedFrames == expectedTransmittedFrames); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + const auto timeoutDeadline = simTime() + SimTime(1, SIMTIME_MS); + ASSERT(hcf->getResponseTimerArrival() == timeoutDeadline); + + hcf->injectAck(); + hcf->injectCts(); + hcf->injectData(); + hcf->injectRts(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == timeoutDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + ASSERT(hcf->failedFrames == expectedFailedFrames - 1); + + wait(SimTime(500, SIMTIME_US)); + hcf->injectAck(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == timeoutDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + ASSERT(hcf->failedFrames == expectedFailedFrames - 1); + + wait(SimTime(500, SIMTIME_US) + SimTime(1, SIMTIME_US)); + ASSERT(!hcf->isResponseWaitActive()); + ASSERT(!hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); ASSERT(hcf->finishedSequences == expectedFinishedSequences); + ASSERT(hcf->failedFrames == expectedFailedFrames); + ASSERT(hcf->lastFinishedAt == timeoutDeadline); } - std::cout << "HCF keeps the active Block Ack response wait scoped by peer, TID, and variant.\n"; + std::cout << "HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout.\n"; endSimulation(); } }; @@ -291,7 +438,7 @@ network Ieee80211BlockAckResponseCorrelationTestNetwork [General] network = Ieee80211BlockAckResponseCorrelationTestNetwork ned-path = .;../../../../src;../../lib -sim-time-limit = 1ms +sim-time-limit = 3ms seed-set = 0 cmdenv-express-mode = true record-vector-results = false @@ -305,4 +452,4 @@ record-scalar-results = false **.mobility.constraintAreaMaxZ = 0m %contains: stdout -HCF keeps the active Block Ack response wait scoped by peer, TID, and variant. +HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout. From bd514272193eb2840652f6c8d76ab8f5d0546a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 11:10:26 +0200 Subject: [PATCH 68/91] ieee80211: centralize Block Ack receive-window arithmetic Block Ack records, receive buffers, and reordering repeated cyclic window comparisons and advancement calculations. Route these decisions through a stateless BlockAckWindow service so their wraparound and half-sequence-space semantics stay consistent while each consumer retains ownership of its state. Preserve the existing comparisons and add focused unit coverage for window edges, sequence wraparound, candidate starts, and the unordered antipodal boundary. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 15 ++--- .../ieee80211/mac/blockack/BlockAckWindow.h | 61 +++++++++++++++++++ .../blockackreordering/BlockAckReordering.cc | 11 ++-- .../mac/blockackreordering/ReceiveBuffer.cc | 10 +-- tests/unit/Ieee80211BlockAckWindow_1.test | 47 ++++++++++++++ 5 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h create mode 100644 tests/unit/Ieee80211BlockAckWindow_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 54d478446b1..41c813a8a6c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" #include "inet/common/stlutils.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" namespace inet { namespace ieee80211 { @@ -26,12 +27,12 @@ void BlockAckRecord::dataFrameReceived(const Ptr& hea // IEEE Std 802.11-2024, 10.25.6.3(b), case 3: an old related MPDU // does not change the Block Ack record. The cyclic comparison below already // restricts the accepted range to [WinStartR, WinStartR + 2047]. - if (!(startingSequenceNumber <= sequenceNumber)) + if (!BlockAckWindow::isAtOrAfter(sequenceNumber, startingSequenceNumber)) return; // Cases 1 and 2: record an in-window MPDU, or advance WinStartR // before recording an MPDU beyond WinEndR (also see 10.25.6.4(c)). - if (startingSequenceNumber + windowSize <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048) - advanceStartingSequenceNumber(sequenceNumber - windowSize + 1); + if (BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, sequenceNumber)) + advanceStartingSequenceNumber(BlockAckWindow::getStartingSequenceNumber(sequenceNumber, windowSize)); acknowledgmentState[SequenceControlField(sequenceNumber.get(), fragmentNumber)] = true; } @@ -40,25 +41,25 @@ bool BlockAckRecord::getAckState(SequenceNumberCyclic sequenceNumber, FragmentNu // The status of MPDUs that are considered “old” and prior to the sequence number // range for which the receiver maintains status shall be reported as successfully // received (i.e., the corresponding bit in the bitmap shall be set to 1). - return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber)) || sequenceNumber < startingSequenceNumber; + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber)) || BlockAckWindow::isBefore(sequenceNumber, startingSequenceNumber); } bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) { // IEEE Std 802.11-2024, 10.25.6.1: bits preceding the maintained // receive-window range are one; missing MPDUs within the range are zero. - return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || sequenceNumber < startingSequenceNumber; + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || BlockAckWindow::isBefore(sequenceNumber, startingSequenceNumber); } void BlockAckRecord::advanceStartingSequenceNumber(SequenceNumberCyclic newStartingSequenceNumber) { // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: advance WinStartR // for a newer related MPDU or BAR SSN, using the 12-bit sequence space. - if (!(startingSequenceNumber < newStartingSequenceNumber)) + if (!BlockAckWindow::isAfter(newStartingSequenceNumber, startingSequenceNumber)) return; auto it = acknowledgmentState.begin(); while (it != acknowledgmentState.end()) { - if (SequenceNumberCyclic(it->first.getSequenceNumber()) < newStartingSequenceNumber) + if (BlockAckWindow::isBefore(SequenceNumberCyclic(it->first.getSequenceNumber()), newStartingSequenceNumber)) it = acknowledgmentState.erase(it); else it++; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h new file mode 100644 index 00000000000..db93d3af02d --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h @@ -0,0 +1,61 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_BLOCKACKWINDOW_H +#define __INET_BLOCKACKWINDOW_H + +#include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" + +namespace inet { +namespace ieee80211 { + +/** + * Shared cyclic-window decisions for Block Ack receive state. + * + * The start and size remain owned by the Block Ack record or receive buffer; + * this class only keeps the sequence-space predicates in one place. + */ +class INET_API BlockAckWindow +{ + public: + static constexpr int HALF_SEQUENCE_SPACE = 2048; + + static bool isBefore(SequenceNumberCyclic sequenceNumber, SequenceNumberCyclic reference) + { + return sequenceNumber < reference; + } + + static bool isAtOrAfter(SequenceNumberCyclic sequenceNumber, SequenceNumberCyclic reference) + { + return reference <= sequenceNumber; + } + + static bool isAfter(SequenceNumberCyclic sequenceNumber, SequenceNumberCyclic reference) + { + return reference < sequenceNumber; + } + + static bool isWithin(SequenceNumberCyclic startingSequenceNumber, int windowSize, SequenceNumberCyclic sequenceNumber) + { + return isAtOrAfter(sequenceNumber, startingSequenceNumber) && isBefore(sequenceNumber, startingSequenceNumber + windowSize); + } + + static bool isBeyond(SequenceNumberCyclic startingSequenceNumber, int windowSize, SequenceNumberCyclic sequenceNumber) + { + return isAtOrAfter(sequenceNumber, startingSequenceNumber + windowSize) && isBefore(sequenceNumber, startingSequenceNumber + HALF_SEQUENCE_SPACE); + } + + static SequenceNumberCyclic getStartingSequenceNumber(SequenceNumberCyclic sequenceNumber, int windowSize) + { + return sequenceNumber - windowSize + 1; + } +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 145604677c1..a3d0ab51133 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -9,6 +9,7 @@ #include +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" #include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" @@ -36,12 +37,12 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived ReorderBuffer framesToPassUp; auto sequenceNumber = dataHeader->getSequenceNumber(); auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); - bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; + bool advancesWindow = BlockAckWindow::isBeyond(startingSequenceNumber, receiveBuffer->getBufferSize(), sequenceNumber); SequenceNumberCyclic newStartingSequenceNumber; if (advancesWindow) { // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): store the future MPDU // before moving WinStartB and releasing complete displaced MSDUs. - newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; + newStartingSequenceNumber = BlockAckWindow::getStartingSequenceNumber(sequenceNumber, receiveBuffer->getBufferSize()); } // The reception of QoS data frames using Normal Ack policy shall not be used by the // recipient to reset the timer to detect Block Ack timeout (see 10.5.4). @@ -148,7 +149,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq // advance NextExpectedSequenceNumber to at least the BAR SSN without // regressing it past consecutively released MSDUs. receiveBuffer->dropFramesUntil(startingSequenceNumber); - if (receiveBuffer->getNextExpectedSequenceNumber() < startingSequenceNumber) + if (BlockAckWindow::isBefore(receiveBuffer->getNextExpectedSequenceNumber(), startingSequenceNumber)) receiveBuffer->setNextExpectedSequenceNumber(startingSequenceNumber); return completePrecedingMpdus; } @@ -167,7 +168,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto currentStartingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); for (int i = 0; i < receiveBuffer->getBufferSize(); i++) { auto sequenceNumber = currentStartingSequenceNumber + i; - if (!(sequenceNumber < startingSequenceNumber)) + if (!BlockAckWindow::isBefore(sequenceNumber, startingSequenceNumber)) break; auto it = buffer.find(sequenceNumber.get()); if (it != buffer.end() && ReceiveBuffer::isComplete(it->second)) @@ -295,7 +296,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists if (earliestFragments.size() > 0) { for (auto it : buffer) { SequenceNumberCyclic currentSeqNum = it.second.at(0)->peekAtFront()->getSequenceNumber(); - if (currentSeqNum < earliestSeqNum) { + if (BlockAckWindow::isBefore(currentSeqNum, earliestSeqNum)) { if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = currentSeqNum; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 29c33010ed4..5592004173d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + namespace inet { namespace ieee80211 { @@ -46,7 +48,7 @@ bool ReceiveBuffer::isComplete(const Fragments& fragments) void ReceiveBuffer::pruneExpiredFragmentSequences() { for (auto it = expiredFragmentSequences.begin(); it != expiredFragmentSequences.end();) { - if (SequenceNumberCyclic(*it) < nextExpectedSequenceNumber) + if (BlockAckWindow::isBefore(SequenceNumberCyclic(*it), nextExpectedSequenceNumber)) it = expiredFragmentSequences.erase(it); else ++it; @@ -63,11 +65,11 @@ bool ReceiveBuffer::canInsertFrame(const Ptr& dataHea { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); - if (!(nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize)) + if (!BlockAckWindow::isWithin(nextExpectedSequenceNumber, bufferSize, sequenceNumber)) return false; int retainedLength = length; for (const auto& entry : buffer) { - if (SequenceNumberCyclic(entry.first) < nextExpectedSequenceNumber) + if (BlockAckWindow::isBefore(SequenceNumberCyclic(entry.first), nextExpectedSequenceNumber)) retainedLength -= entry.second.size(); } // IEEE Std 802.11-2024, 9.4.1.13, footnote 26: each fragment @@ -136,7 +138,7 @@ void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) { auto it = buffer.begin(); while (it != buffer.end()) { - if (SequenceNumberCyclic(it->first) < sequenceNumber) { + if (BlockAckWindow::isBefore(SequenceNumberCyclic(it->first), sequenceNumber)) { length -= it->second.size(); for (auto fragment : it->second) delete fragment; diff --git a/tests/unit/Ieee80211BlockAckWindow_1.test b/tests/unit/Ieee80211BlockAckWindow_1.test new file mode 100644 index 00000000000..52984bae589 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckWindow_1.test @@ -0,0 +1,47 @@ +%description: +Validate the shared cyclic receive-window decisions used by Block Ack records, +receive buffers, and reordering. +IEEE Std 802.11-2024, 10.25.6.3, 10.25.6.4, and 10.25.6.6.2.1. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +%activity: +{ + const auto startingSequenceNumber = SequenceNumberCyclic(4094); + const int windowSize = 4; + + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(4094))); + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(4095))); + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(0))); + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(1))); + ASSERT(!BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(2))); + ASSERT(!BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(2046))); + + ASSERT(BlockAckWindow::isBefore(SequenceNumberCyclic(4093), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isBefore(startingSequenceNumber, startingSequenceNumber)); + ASSERT(BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(4094), startingSequenceNumber)); + ASSERT(BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(0), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(4093), startingSequenceNumber)); + + ASSERT(BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, SequenceNumberCyclic(2))); + ASSERT(BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, SequenceNumberCyclic(2045))); + ASSERT(!BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, SequenceNumberCyclic(2046))); + ASSERT(!BlockAckWindow::isAfter(SequenceNumberCyclic(2046), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isBefore(SequenceNumberCyclic(2046), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(2046), startingSequenceNumber)); + + ASSERT(BlockAckWindow::getStartingSequenceNumber(SequenceNumberCyclic(2), windowSize) == SequenceNumberCyclic(4095)); + ASSERT(BlockAckWindow::getStartingSequenceNumber(SequenceNumberCyclic(0), windowSize) == SequenceNumberCyclic(4093)); + EV << "Shared Block Ack cyclic receive-window predicates preserve wraparound and the antipodal boundary.\n"; +} + +EV << ".\n"; + +%contains: stdout +Shared Block Ack cyclic receive-window predicates preserve wraparound and the antipodal boundary. +. From 98b70d448ab9d023cb6cc933c6c9259209940a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 11:41:51 +0200 Subject: [PATCH 69/91] ieee80211: Enforce compressed BAR eligibility before state changes A compressed BAR with a nonzero fragment number could advance the receive window before the acknowledgment policy rejected its response. Share recipient acceptance checks before reorder processing and response selection, requiring an established immediate agreement with snapshotted local HT operation and configured peer compressed Block Ack support. Sparse outstanding sequences such as 0 and 100 could leave sequence 100 waiting for a response that cannot represent it. Reject compressed selection when the matching set exceeds the 64-position bitmap and limit transmitted-BAR acknowledgment transitions to that window. Add module regressions for unchanged receive state and no response after rejected BARs, capability gates, sparse and wrapping sequences, timeout, and subsequent requests for frames outside the first window. Validation: debug build, two focused unit tests, four focused module tests, and scoped architecture, naming, and seal checks pass. The legacy MacQosWithBlockAck fingerprint mismatch is identical on original HEAD; no fingerprint baseline changes are included. --- .../mac/blockack/OneTidBlockAckReqVariant.h | 16 ++ .../mac/blockack/RecipientBlockAckAgreement.h | 3 + .../RecipientBlockAckAgreementHandler.cc | 1 + .../RecipientBlockAckAgreementPolicy.cc | 9 + .../RecipientBlockAckAgreementPolicy.h | 7 +- .../RecipientBlockAckAgreementPolicy.ned | 2 + .../IRecipientBlockAckAgreementPolicy.h | 1 + .../mac/originator/OriginatorQosAckPolicy.cc | 12 + .../ieee80211/mac/originator/QosAckHandler.cc | 5 +- .../mac/recipient/RecipientQosAckPolicy.cc | 7 +- .../recipient/RecipientQosMacDataService.cc | 22 +- .../Ieee80211CompressedBlockAckRuntime.test | 4 + ...Ieee80211CompressedBlockAckValidation.test | 256 ++++++++++++++++++ tests/unit/Ieee80211AddbaTransaction_1.test | 1 + tests/unit/Ieee80211CompressedBlockAck_1.test | 3 + 15 files changed, 331 insertions(+), 18 deletions(-) create mode 100644 tests/module/Ieee80211CompressedBlockAckValidation.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h index cc340c652a7..f0ed03e382a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -10,6 +10,7 @@ #include #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -40,6 +41,21 @@ inline std::optional getOneTidBlockAckReqDetails(const return std::nullopt; } +// Validate before either receive-window mutation or response generation. +// IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6: compressed BARs have +// fragment number zero and require an established HT-immediate agreement. +inline bool isAcceptedOneTidBlockAckReq(const Ptr& request, const RecipientBlockAckAgreement *agreement) +{ + if (agreement == nullptr) + return false; + if (dynamicPtrCast(request)) + return !agreement->isInactivityExpired(); + if (auto compressed = dynamicPtrCast(request)) + return compressed->getFragmentNumber() == 0 && agreement->getIsCompressedBlockAckSupported() && + agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported(); + return false; +} + // IEEE Std 802.11-2024, 9.3.1.7, 9.3.1.8, and 10.25.5: correlate the BAR RA // with the BA TA, TID, and selected BlockAck variant before accepting a response. inline bool isMatchingOneTidBlockAckResponse(const OneTidBlockAckReqDetails& blockAckReqDetails, const Ptr& blockAck) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index f0d5a59e191..31febb4f507 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,6 +21,7 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; bool isAddbaResponseSent = false; + bool isCompressedBlockAckSupported = false; bool isDelayedBlockAckPolicySupported = false; simtime_t expirationTime = -1; // The agreement stays installed until the timeout DELBA is transmitted; @@ -44,6 +45,8 @@ class INET_API RecipientBlockAckAgreement : public cObject if (!inactivityExpired) expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual bool getIsCompressedBlockAckSupported() const { return isCompressedBlockAckSupported; } + virtual void setIsCompressedBlockAckSupported(bool supported) { isCompressedBlockAckSupported = supported; } virtual bool getIsAddbaResponseSent() const { return isAddbaResponseSent; } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 1d7ed7641c4..29b7078d665 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -206,6 +206,7 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAd } else blockAckAgreements[id] = agreement; + agreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaRequest->getTransmitterAddress())); agreement->addbaResposneSent(); agreement->setIsDelayedBlockAckPolicySupported(addbaResponse->getBlockAckPolicy() == 0); scheduleInactivityTimer(agreementHandlerCallback); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index 73813feaa5d..062c5c519f4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc @@ -14,7 +14,11 @@ Define_Module(RecipientBlockAckAgreementPolicy); void RecipientBlockAckAgreementPolicy::initialize(int stage) { + ModeSetListener::initialize(stage); if (stage == INITSTAGE_LOCAL) { + localCompressedBlockAckSupported = par("localCompressedBlockAckSupported"); + for (const auto& address : cStringTokenizer(par("compressedBlockAckPeerAddresses")).asVector()) + compressedBlockAckPeerAddresses.insert(MacAddress(address.c_str())); isDelayedBlockAckPolicySupported = par("delayedAckPolicySupported"); isAMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); @@ -22,6 +26,11 @@ void RecipientBlockAckAgreementPolicy::initialize(int stage) } } +bool RecipientBlockAckAgreementPolicy::isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const +{ + return modeSet != nullptr && modeSet->isHtOperationSupported() && localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.count(peerAddress) != 0; +} + bool RecipientBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaReq) { return true; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h index 995fcc29fdb..16b4da33f8e 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h @@ -8,17 +8,19 @@ #ifndef __INET_RECIPIENTBLOCKACKAGREEMENTPOLICY_H #define __INET_RECIPIENTBLOCKACKAGREEMENTPOLICY_H -#include "inet/common/SimpleModule.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { namespace ieee80211 { -class INET_API RecipientBlockAckAgreementPolicy : public SimpleModule, public IRecipientBlockAckAgreementPolicy +class INET_API RecipientBlockAckAgreementPolicy : public ModeSetListener, public IRecipientBlockAckAgreementPolicy { protected: int maximumAllowedBufferSize = -1; + bool localCompressedBlockAckSupported = false; + std::set compressedBlockAckPeerAddresses; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; simtime_t blockAckTimeoutValue = -1; @@ -34,6 +36,7 @@ class INET_API RecipientBlockAckAgreementPolicy : public SimpleModule, public IR virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool aMsduSupported() const override { return isAMsduSupported; } virtual bool delayedBlockAckPolicySupported() const override { return isDelayedBlockAckPolicySupported; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override; virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned index 8c41285b009..ff84122b1d3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned @@ -17,6 +17,8 @@ simple RecipientBlockAckAgreementPolicy extends SimpleModule like IRecipientBloc { parameters: @class(RecipientBlockAckAgreementPolicy); + bool localCompressedBlockAckSupported = default(false); // Requires HT operation in the active mode set + string compressedBlockAckPeerAddresses = default(""); // Configured peer capability input, snapshotted at ADDBA acceptance bool delayedAckPolicySupported = default(false); bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h index 48009a67161..3f03c6532dc 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h @@ -24,6 +24,7 @@ class INET_API IRecipientBlockAckAgreementPolicy virtual simtime_t getBlockAckTimeoutValue() const = 0; virtual bool aMsduSupported() const = 0; virtual bool delayedBlockAckPolicySupported() const = 0; + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const = 0; virtual int getMaximumAllowedBufferSize() const = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 626a08819be..bdb9b424b15 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + #include #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" @@ -74,14 +76,24 @@ bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vectorgetIsCompressedBlockAckSupported() || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) return false; bool hasMatchingOutstandingFrame = false; + SequenceNumberCyclic startingSequenceNumber; for (auto frame : outstandingFrames) { auto header = dynamicPtrCast(frame->peekAtFront()); if (header == nullptr || header->getReceiverAddress() != agreement->getReceiverAddr() || header->getTid() != agreement->getTid()) continue; + if (!hasMatchingOutstandingFrame || header->getSequenceNumber() < startingSequenceNumber) + startingSequenceNumber = header->getSequenceNumber(); hasMatchingOutstandingFrame = true; if (header->getFragmentNumber() != 0 || header->getMoreFragments()) return false; } + for (auto frame : outstandingFrames) { + auto header = dynamicPtrCast(frame->peekAtFront()); + if (header != nullptr && header->getReceiverAddress() == agreement->getReceiverAddr() && header->getTid() == agreement->getTid()) { + if (!BlockAckWindow::isWithin(startingSequenceNumber, 64, header->getSequenceNumber())) + return false; + } + } return hasMatchingOutstandingFrame; } diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index e33d1fffbf1..4cbff5f7074 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + namespace inet { namespace ieee80211 { @@ -227,7 +229,8 @@ void QosAckHandler::processTransmittedBlockAckReq(const Ptr(blockAckReq)) { if (receiverAddress == blockAckReq->getReceiverAddress() && compressedBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = compressedBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum && seqCtrlField.getFragmentNumber() == 0) // TODO ASSERT(seqCtrlField.second == 0)? + // IEEE Std 802.11-2024, 10.25.6.1: the non-HE bitmap covers 64 sequence numbers. + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && BlockAckWindow::isWithin(startingSeqNum, 64, SequenceNumberCyclic(seqCtrlField.getSequenceNumber())) && seqCtrlField.getFragmentNumber() == 0) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 3e81c4cb221..647defe934c 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" #include "inet/common/ModuleAccess.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { @@ -63,7 +64,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { if (dynamicPtrCast(blockAckReq)) { - return agreement != nullptr && !agreement->isInactivityExpired(); + return isAcceptedOneTidBlockAckReq(blockAckReq, agreement); // TODO The Basic BlockAckReq frame shall be discarded if all MSDUs referenced by this // frame have been discarded from the transmit buffer due to expiry of their lifetime limit. } @@ -77,9 +78,7 @@ bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const PtrgetFragmentNumber() != 0) - return false; - return agreement != nullptr && agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported(); + return isAcceptedOneTidBlockAckReq(blockAckReq, agreement); } // diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 0911cb8e0cf..85394b563db 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -249,19 +249,19 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); + auto blockAckReqDetails = getOneTidBlockAckReqDetails(controlHeader); + RecipientBlockAckAgreement *agreement = nullptr; + if (blockAckReqDetails) { + auto originatorAddr = blockAckReqDetails->blockAckReq->getTransmitterAddress(); + agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(blockAckReqDetails->tid, originatorAddr); + if (!isAcceptedOneTidBlockAckReq(blockAckReqDetails->blockAckReq, agreement)) + return {}; + } expireReceiveLifetime(); - if (auto blockAckReqDetails = getOneTidBlockAckReqDetails(controlHeader)) { + if (blockAckReqDetails) { BlockAckReordering::ReorderBuffer frames; - if (blockAckReordering) { - MacAddress originatorAddr = blockAckReqDetails->blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(blockAckReqDetails->tid, originatorAddr); - if (agreement) - frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReqDetails->blockAckReq); - else { - scheduleReceiveLifetimeTimer(); - return std::vector(); - } - } + if (blockAckReordering) + frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReqDetails->blockAckReq); std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation for (auto it : frames) { diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test index 039df324eb4..7383d8ed01f 100644 --- a/tests/module/Ieee80211CompressedBlockAckRuntime.test +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -23,11 +23,15 @@ sim-time-limit = 1.05s **.opMode = "n(mixed-2.4Ghz)" **.originatorBlockAckAgreementPolicy.localCompressedBlockAckSupported = true +**.recipientBlockAckAgreementPolicy.localCompressedBlockAckSupported = true *.cliHost.wlan[*].address = "10:00:00:00:00:01" *.srvHost.wlan[*].address = "10:00:00:00:00:02" *.cliHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" +*.cliHost.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" *.ap.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:01 10:00:00:00:00:02" +*.ap.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:01 10:00:00:00:00:02" *.srvHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" +*.srvHost.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" **.cmdenv-log-level = info %contains: stdout diff --git a/tests/module/Ieee80211CompressedBlockAckValidation.test b/tests/module/Ieee80211CompressedBlockAckValidation.test new file mode 100644 index 00000000000..b8eb4b6e752 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckValidation.test @@ -0,0 +1,256 @@ +%description: +Validate compressed BARs before receive-window mutation and response generation, +with configured peer capability and HT operation snapshotted at ADDBA acceptance. +Sparse and wrapping transmit sets must never strand frames beyond bitmap offset 63. +IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.1 (non-HE bitmap length 64). + +%file: TestCompressedBlockAckValidation.cc +#include + +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" + +using namespace inet; +using namespace inet::ieee80211; + +static const MacAddress PEER("02:00:00:00:00:01"); +static const MacAddress LOCAL("02:00:00:00:00:02"); + +class InspectReordering : public BlockAckReordering +{ + public: + ReceiveBuffer *getBuffer() { return receiveBuffers.at(std::make_pair(5, PEER)); } +}; + +class InspectDataService : public RecipientQosMacDataService +{ + protected: + virtual void initialize() override { + RecipientQosMacDataService::initialize(); + delete blockAckReordering; + blockAckReordering = new InspectReordering(); + } + public: + ReceiveBuffer *getBuffer() { return static_cast(blockAckReordering)->getBuffer(); } +}; +Define_Module(InspectDataService); + +class RecipientPolicy : public RecipientBlockAckAgreementPolicy +{ + public: + void configure(bool localSupport, bool peerSupport, bool ht) { + maximumAllowedBufferSize = 64; + blockAckTimeoutValue = 0; + localCompressedBlockAckSupported = localSupport; + compressedBlockAckPeerAddresses.clear(); + if (peerSupport) + compressedBlockAckPeerAddresses.insert(PEER); + modeSet = const_cast(physicallayer::Ieee80211ModeSet::getModeSet(ht ? "n(mixed-2.4Ghz)" : "g(mixed)")); + } +}; + +class TestRecipientAckPolicy : public RecipientQosAckPolicy +{ + public: + virtual simtime_t computeBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } +}; + +class Callback : public IProcedureCallback +{ + public: + int responses = 0; + virtual void processMgmtFrame(Packet *packet, const Ptr&) override { delete packet; } + virtual void transmitControlResponseFrame(Packet *packet, const Ptr& header, Packet *, const Ptr&) override { + ASSERT(dynamicPtrCast(header) != nullptr); + responses++; + delete packet; + } +}; + +class CompressedBlockAckValidation : public SimpleModule +{ + protected: + Ptr dataHeader(int sequence, int fragment = 0, bool more = false) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setTransmitterAddress(PEER); + header->setReceiverAddress(LOCAL); + header->setTid(5); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setFragmentNumber(fragment); + header->setMoreFragments(more); + return header; + } + + void checkRecipient(bool localSupport, bool peerSupport, bool ht, int fragment) { + auto service = check_and_cast(cModuleType::get("InspectDataService")->create("service", this)); + service->callInitialize(); + RecipientBlockAckAgreementHandler handler; + RecipientPolicy policy; + policy.configure(localSupport, peerSupport, ht); + Callback callback; + auto addba = makeShared(); + addba->setTransmitterAddress(PEER); + addba->setReceiverAddress(LOCAL); + addba->setTid(5); + addba->setDialogToken(1); + addba->setBufferSize(64); + addba->setStartingSequenceNumber(SequenceNumberCyclic(0)); + addba->setBlockAckPolicy(true); + addba->setBlockAckTimeoutValue(0); + auto agreement = handler.processReceivedAddbaRequest(addba, &policy, &callback, nullptr); + ASSERT(agreement != nullptr); + ASSERT(agreement->getIsCompressedBlockAckSupported() == (localSupport && peerSupport && ht)); + // Subsequent policy changes do not retroactively change this agreement. + policy.configure(!localSupport, !peerSupport, !ht); + ASSERT(agreement->getIsCompressedBlockAckSupported() == (localSupport && peerSupport && ht)); + + auto completeHeader = dataHeader(1); + auto incompleteHeader = dataHeader(2, 0, true); + auto complete = new Packet("complete", completeHeader); + auto incomplete = new Packet("incomplete", incompleteHeader); + ASSERT(service->dataFrameReceived(complete, completeHeader, &handler).empty()); + ASSERT(service->dataFrameReceived(incomplete, incompleteHeader, &handler).empty()); + auto buffer = service->getBuffer(); + auto savedBuffer = buffer->getBuffer(); + auto savedCursor = buffer->getNextExpectedSequenceNumber(); + auto savedStart = agreement->getStartingSequenceNumber(); + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(3)); + request->setFragmentNumber(fragment); + auto packet = new Packet("rejectedBar", request); + ASSERT(service->controlFrameReceived(packet, request, &handler).empty()); + TestRecipientAckPolicy ackPolicy; + RecipientBlockAckProcedure procedure; + procedure.processReceivedBlockAckReq(packet, request, &ackPolicy, &handler, &callback); + ASSERT(callback.responses == 0); + ASSERT(buffer->getBuffer() == savedBuffer); + ASSERT(buffer->getLength() == 2); + ASSERT(buffer->getNextExpectedSequenceNumber() == savedCursor); + ASSERT(agreement->getStartingSequenceNumber() == savedStart); + ASSERT(agreement->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(1))); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2), 0)); + ASSERT(!agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2), 1)); + delete packet; + + if (localSupport && peerSupport && ht) { + auto valid = makeShared(*request); + valid->setFragmentNumber(0); + auto validPacket = new Packet("validBar", valid); + auto released = service->controlFrameReceived(validPacket, valid, &handler); + ASSERT(released.size() == 1 && released.front() == complete); + ASSERT(buffer->getLength() == 0); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(3)); + procedure.processReceivedBlockAckReq(validPacket, valid, &ackPolicy, &handler, &callback); + ASSERT(callback.responses == 1); + for (auto frame : released) { take(frame); delete frame; } + delete validPacket; + } + service->deleteModule(); + } + + void checkSparse(int start, bool timeout) { + OriginatorBlockAckAgreement agreement(LOCAL, 5, SequenceNumberCyclic(start), 64, false, false, 0, 1); + agreement.setIsAddbaResponseReceived(true); + agreement.setIsCompressedBlockAckSupported(true); + OriginatorQosAckPolicy policy; + QosAckHandler handler; + std::vector frames; + for (int offset : {0, 63, 64, 100}) { + auto header = dataHeader((start + offset) % 4096); + frames.push_back(new Packet("outstanding", header)); + handler.frameGotInProgress(header); + handler.processTransmittedDataOrMgmtFrame(header); + } + ASSERT(policy.isCompressedBlockAckReq({frames[0], frames[1]}, &agreement)); + ASSERT(!policy.isCompressedBlockAckReq({frames[0], frames[2]}, &agreement)); + ASSERT(!policy.isCompressedBlockAckReq({frames[0], frames[3]}, &agreement)); + ASSERT(!policy.isCompressedBlockAckReq(frames, &agreement)); + auto bar = makeShared(); + bar->setReceiverAddress(LOCAL); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(start)); + handler.processTransmittedBlockAckReq(bar); + for (int i = 0; i < 4; i++) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == + (i < 2 ? QosAckHandler::Status::WAITING_FOR_BLOCK_ACK : QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED)); + if (timeout) + handler.processFailedBlockAckReq(bar); + else { + auto ba = makeShared(); + ba->setTransmitterAddress(LOCAL); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(start)); + ba->setBlockAckBitmap(BitVector(std::vector(8, 255))); + ASSERT(handler.processReceivedBlockAck(ba).size() == 2); + } + for (int i = 0; i < 4; i++) { + auto header = frames[i]->peekAtFront(); + ASSERT(handler.getQoSDataAckStatus(header) == (i >= 2 ? QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED : + timeout ? QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED : QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED)); + if (i >= 2) + ASSERT(handler.isOutstandingFrame(header)); + } + // A later BAR can request both residual frames. + auto nextBar = makeShared(*bar); + nextBar->setStartingSequenceNumber(SequenceNumberCyclic((start + 64) % 4096)); + handler.processTransmittedBlockAckReq(nextBar); + for (int i : {2, 3}) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + for (auto frame : frames) + delete frame; + } + + virtual void initialize() override { + checkRecipient(true, true, true, 1); + checkRecipient(true, false, true, 0); + checkRecipient(true, true, false, 0); + checkRecipient(false, true, true, 0); + for (int start : {0, 4090}) + for (bool timeout : {false, true}) + checkSparse(start, timeout); + std::cout << "Compressed BAR validation preserves receive state and limits transmit state to 64 positions.\n"; + } +}; +Define_Module(CompressedBlockAckValidation); + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +module InspectDataService extends RecipientQosMacDataService +{ + parameters: + @class(::InspectDataService); +} +simple CompressedBlockAckValidation extends SimpleModule +{ + parameters: + @class(::CompressedBlockAckValidation); +} +network TestNetwork +{ + submodules: + test: CompressedBlockAckValidation; +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Compressed BAR validation preserves receive state and limits transmit state to 64 positions. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index aad0aaffb02..467c35bfe65 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -93,6 +93,7 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool aMsduSupported() const override { return aMsduSupportedValue; } virtual bool delayedBlockAckPolicySupported() const override { return delayedBlockAckPolicySupportedValue; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress&) const override { return false; } virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 411bf47b060..d584316d8e5 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -157,6 +157,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag header->setType(ST_DATA_WITH_QOS); header->setReceiverAddress(receiverAddress); header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(100)); header->setFragmentNumber(fragmentNumber); header->setMoreFragments(moreFragments); return new Packet("outstanding", header); @@ -267,6 +268,7 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement emptyAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); emptyAgreement.addbaResposneSent(); + emptyAgreement.setIsCompressedBlockAckSupported(true); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(100)); ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &emptyAgreement)); @@ -682,6 +684,7 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat RecipientBlockAckAgreement delayedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.addbaResposneSent(); + delayedAgreement.setIsCompressedBlockAckSupported(true); delayedAgreement.setIsDelayedBlockAckPolicySupported(true); ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.setIsDelayedBlockAckPolicySupported(false); From b872e83c72d07ae3257d683760dd598fb3868960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 12:01:06 +0200 Subject: [PATCH 70/91] ieee80211: Reject ineligible BARs before renewing agreement lifetime Malformed compressed BARs were rejected by receive-state and response processing but still renewed the recipient inactivity deadline through blockAckReqReceived. Apply the shared eligibility check in the agreement handler before updating the deadline or scheduling the inactivity timer. Extend the module regression with elapsed simulation time to verify that malformed and capability-ineligible BARs preserve the existing deadline, while valid compressed and Basic BARs still renew it. The new assertion fails before the fix and passes afterward. Validation: debug build, compressed BAR validation and inactivity timer module tests, ADDBA transaction unit test, scoped architecture check, and diff whitespace check pass. --- .../RecipientBlockAckAgreementHandler.cc | 3 +- ...Ieee80211CompressedBlockAckValidation.test | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 29b7078d665..8f9efe1d815 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" @@ -61,7 +62,7 @@ void RecipientBlockAckAgreementHandler::blockAckReqReceived(const PtrgetTransmitterAddress()); - if (agreement != nullptr) { + if (isAcceptedOneTidBlockAckReq(blockAckReq, agreement)) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } diff --git a/tests/module/Ieee80211CompressedBlockAckValidation.test b/tests/module/Ieee80211CompressedBlockAckValidation.test index b8eb4b6e752..f48145f2844 100644 --- a/tests/module/Ieee80211CompressedBlockAckValidation.test +++ b/tests/module/Ieee80211CompressedBlockAckValidation.test @@ -76,6 +76,8 @@ class Callback : public IProcedureCallback class CompressedBlockAckValidation : public SimpleModule { protected: + RecipientBlockAckAgreementHandler lifetimeHandler; + RecipientBlockAckAgreement *lifetimeAgreement = nullptr; Ptr dataHeader(int sequence, int fragment = 0, bool more = false) { auto header = makeShared(); header->setType(ST_DATA_WITH_QOS); @@ -211,7 +213,61 @@ class CompressedBlockAckValidation : public SimpleModule delete frame; } + virtual void handleMessage(cMessage *message) override { + auto savedDeadline = lifetimeAgreement->getExpirationTime(); + ASSERT(savedDeadline > simTime()); + if (message->getKind() == 0) { + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setFragmentNumber(1); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == savedDeadline); + + request->setFragmentNumber(0); + lifetimeAgreement->setIsCompressedBlockAckSupported(false); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == savedDeadline); + + lifetimeAgreement->setIsCompressedBlockAckSupported(true); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == simTime() + SimTime(1)); + ASSERT(lifetimeAgreement->getExpirationTime() > savedDeadline); + message->setKind(1); + scheduleAt(simTime() + SimTime(1, SIMTIME_MS), message); + } + else { + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == simTime() + SimTime(1)); + ASSERT(lifetimeAgreement->getExpirationTime() > savedDeadline); + delete message; + std::cout << "Rejected BARs preserve inactivity deadlines; valid BARs renew them.\n"; + } + } + virtual void initialize() override { + RecipientPolicy policy; + policy.configure(true, true, true); + Callback callback; + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTid(5); + request->setDialogToken(1); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setBlockAckPolicy(true); + request->setBlockAckTimeoutValue(SimTime(1)); + lifetimeAgreement = lifetimeHandler.processReceivedAddbaRequest(request, &policy, &callback, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == SimTime(1)); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("checkBarLifetime")); checkRecipient(true, true, true, 1); checkRecipient(true, false, true, 0); checkRecipient(true, true, false, 0); @@ -254,3 +310,6 @@ record-scalar-results = false %contains: stdout Compressed BAR validation preserves receive state and limits transmit state to 64 positions. + +%contains: stdout +Rejected BARs preserve inactivity deadlines; valid BARs renew them. From b43d3b8a494bc5daa1dc7397217d42a69409d4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 00:29:34 +0200 Subject: [PATCH 71/91] ieee80211: correlate Block Ack responses by starting sequence A delayed Block Ack from an earlier BAR to the same peer and TID can complete a later receive step and acknowledge its outstanding frames. Require the response starting sequence number to match the active Basic or Compressed BAR. Extend the HCF response correlation test with successive BARs whose bitmap windows overlap. Assert that the stale response preserves the timeout and acknowledgment statuses, and that a matching response still succeeds. --- .../mac/blockack/OneTidBlockAckReqVariant.h | 8 +- ...eee80211BlockAckResponseCorrelation_1.test | 73 ++++++++++++++++--- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h index f0ed03e382a..b46c18cbb36 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -57,18 +57,20 @@ inline bool isAcceptedOneTidBlockAckReq(const Ptr& r } // IEEE Std 802.11-2024, 9.3.1.7, 9.3.1.8, and 10.25.5: correlate the BAR RA -// with the BA TA, TID, and selected BlockAck variant before accepting a response. +// with the BA TA, TID, starting sequence number, and selected BlockAck variant. inline bool isMatchingOneTidBlockAckResponse(const OneTidBlockAckReqDetails& blockAckReqDetails, const Ptr& blockAck) { if (blockAckReqDetails.blockAckReq->getReceiverAddress() != blockAck->getTransmitterAddress()) return false; if (blockAckReqDetails.variant == OneTidBlockAckReqVariant::BASIC) { auto basicBlockAck = dynamicPtrCast(blockAck); - return basicBlockAck != nullptr && basicBlockAck->getTidInfo() == blockAckReqDetails.tid; + return basicBlockAck != nullptr && basicBlockAck->getTidInfo() == blockAckReqDetails.tid && + basicBlockAck->getStartingSequenceNumber() == blockAckReqDetails.startingSequenceNumber; } else { auto compressedBlockAck = dynamicPtrCast(blockAck); - return compressedBlockAck != nullptr && compressedBlockAck->getTidInfo() == blockAckReqDetails.tid; + return compressedBlockAck != nullptr && compressedBlockAck->getTidInfo() == blockAckReqDetails.tid && + compressedBlockAck->getStartingSequenceNumber() == blockAckReqDetails.startingSequenceNumber; } } diff --git a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test index 64255a2e979..9f991fe50c7 100644 --- a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test +++ b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test @@ -2,7 +2,7 @@ Exercise the live HCF lower-frame entry while a Block Ack response step is waiting. An ACK, CTS, data frame, or other control frame addressed to this station, as well as a Block Ack for another active peer, the wrong TID, or the -wrong Basic/Compressed variant, must leave the current response wait and its +wrong Basic/Compressed variant or starting sequence number, must leave the current response wait and its timeout armed. The matching response then completes that same frame sequence, while an unmatched wait ends only when its response timer expires. @@ -25,6 +25,7 @@ while an unmatched wait ends only when its response timer expires. #include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" namespace inet { namespace ieee80211 { @@ -53,6 +54,7 @@ class TestOriginatorQosAckPolicy : public IOriginatorQoSAckPolicy { public: bool compressed = true; + SequenceNumber startingSequenceNumber = CURRENT_SSN; virtual bool isAckNeeded(const Ptr&) const override { return true; } virtual AckPolicy computeAckPolicy(Packet *, const Ptr&, OriginatorBlockAckAgreement *) const override { return BLOCK_ACK; } @@ -60,7 +62,7 @@ class TestOriginatorQosAckPolicy : public IOriginatorQoSAckPolicy virtual bool isBlockAckPolicyEligibleFrame(Packet *, const Ptr&) const override { return true; } virtual std::tuple computeBlockAckReqParameters(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { - return std::make_tuple(PEER_ONE, SequenceNumberCyclic(CURRENT_SSN), CURRENT_TID); + return std::make_tuple(PEER_ONE, SequenceNumberCyclic(startingSequenceNumber), CURRENT_TID); } virtual bool isCompressedBlockAckReq(const std::vector&, OriginatorBlockAckAgreement *) const override { return compressed; } virtual simtime_t getAckTimeout(Packet *, const Ptr&) const override { return SimTime(1, SIMTIME_MS); } @@ -139,9 +141,20 @@ class TestHcfBlockAckResponseCorrelation : public Hcf frameSequenceHandler->transmissionComplete(); } - virtual void originatorProcessTransmittedFrame(Packet *) override {} - virtual void originatorProcessReceivedFrame(Packet *, Packet *) override { acceptedResponses++; } - virtual void originatorProcessFailedFrame(Packet *) override { failedFrames++; } + virtual void originatorProcessTransmittedFrame(Packet *packet) override + { + ackHandler.processTransmittedBlockAckReq(packet->peekAtFront()); + } + virtual void originatorProcessReceivedFrame(Packet *packet, Packet *) override + { + acceptedResponses++; + ackHandler.processReceivedBlockAck(packet->peekAtFront()); + } + virtual void originatorProcessFailedFrame(Packet *packet) override + { + failedFrames++; + ackHandler.processFailedBlockAckReq(packet->peekAtFront()); + } virtual void frameSequenceFinished() override { finishedSequences++; @@ -154,6 +167,7 @@ class TestHcfBlockAckResponseCorrelation : public Hcf } public: + QosAckHandler ackHandler; int transmittedFrames = 0; int acceptedResponses = 0; int failedFrames = 0; @@ -171,20 +185,23 @@ class TestHcfBlockAckResponseCorrelation : public Hcf frameSequenceHandler->startFrameSequence(new BlockAckReqBlockAckFs(), context, this); } - void injectBlockAck(const MacAddress& transmitterAddress, Tid tid, bool compressed) + void injectBlockAck(const MacAddress& transmitterAddress, Tid tid, bool compressed, SequenceNumber startingSequenceNumber = CURRENT_SSN) { Enter_Method("injectBlockAck"); Ptr blockAck; if (compressed) { auto compressedBlockAck = makeShared(); compressedBlockAck->setTidInfo(tid); - compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + compressedBlockAck->setBlockAckBitmap(BitVector(std::vector(8, 255))); blockAck = compressedBlockAck; } else { auto basicBlockAck = makeShared(); basicBlockAck->setTidInfo(tid); - basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + for (int i = 0; i < 64; i++) + basicBlockAck->setBlockAckBitmap(i, BitVector(std::vector(2, 255))); blockAck = basicBlockAck; } blockAck->setReceiverAddress(localAddress); @@ -348,6 +365,21 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(hcf->failedFrames == expectedFailedFrames); ASSERT(hcf->finishedSequences == expectedFinishedSequences); + // The next BAR advances by one, so the old response bitmap still + // covers these frames and would incorrectly acknowledge them. + ackPolicy.startingSequenceNumber = CURRENT_SSN + 1; + std::vector> waitingHeaders; + for (int offset : {1, 2}) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(PEER_ONE); + header->setTid(CURRENT_TID); + header->setSequenceNumber(SequenceNumberCyclic(CURRENT_SSN + offset)); + header->setAckPolicy(BLOCK_ACK); + hcf->ackHandler.frameGotInProgress(header); + hcf->ackHandler.processTransmittedDataOrMgmtFrame(header); + waitingHeaders.push_back(header); + } // A fresh BAR wait with no matching response must stay alive // through the same unrelated traffic and finish only at timeout. expectedTransmittedFrames = hcf->transmittedFrames + 1; @@ -373,7 +405,13 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); ASSERT(hcf->failedFrames == expectedFailedFrames - 1); + for (auto header : waitingHeaders) + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); wait(SimTime(500, SIMTIME_US)); + // A delayed duplicate of the first BA arrives during the second wait. + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, compressed, CURRENT_SSN); + for (auto header : waitingHeaders) + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); hcf->injectAck(); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); @@ -390,8 +428,23 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(hcf->finishedSequences == expectedFinishedSequences); ASSERT(hcf->failedFrames == expectedFailedFrames); ASSERT(hcf->lastFinishedAt == timeoutDeadline); + for (auto header : waitingHeaders) { + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + hcf->ackHandler.processTransmittedDataOrMgmtFrame(header); + } + // A response with the current SSN must still complete the receive + // step and update the same acknowledgment statuses. + hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, compressed, ackPolicy.startingSequenceNumber); + ASSERT(!hcf->isResponseWaitActive()); + ASSERT(!hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses + 1); + for (auto header : waitingHeaders) { + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED); + hcf->ackHandler.dropFrame(header); + } } - std::cout << "HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout.\n"; + std::cout << "HCF keeps BAR response waits scoped by expected type, peer, TID, SSN, and variant until match or timeout.\n"; endSimulation(); } }; @@ -452,4 +505,4 @@ record-scalar-results = false **.mobility.constraintAreaMaxZ = 0m %contains: stdout -HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout. +HCF keeps BAR response waits scoped by expected type, peer, TID, SSN, and variant until match or timeout. From c8ca2a76289277c0f94f8506075f0b28f3888bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 00:29:38 +0200 Subject: [PATCH 72/91] ieee80211: require HT operation for compressed Block Ack Local and per-peer configuration can enable compressed Block Ack agreements even when the active mode set is legacy. Require a non-null mode set supporting HT operation before advertising peer compressed Block Ack support. Cover capability checks and agreement creation with null, legacy a/b/g, and HT mode sets while retaining local and per-peer gating coverage. --- .../mac/blockack/OriginatorBlockAckAgreementPolicy.h | 2 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index c2ae595977e..00aae6eeb4b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -45,7 +45,7 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetModuleBase, pub virtual bool isMsduSupported() const override { return aMsduSupported; } virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool isDelayedAckPolicySupported() const override { return delayedAckPolicySupported; } - virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override { return localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.find(peerAddress) != compressedBlockAckPeerAddresses.end(); } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override { return modeSet != nullptr && modeSet->isHtOperationSupported() && localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.find(peerAddress) != compressedBlockAckPeerAddresses.end(); } virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index d584316d8e5..c8c5cb7c469 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -73,6 +73,7 @@ class TestOriginatorBlockAckAgreementPolicy : public OriginatorBlockAckAgreement public: void configureCompressedBlockAckCapability(bool localSupport, std::initializer_list peerAddresses) { + applyModeSet(physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); localCompressedBlockAckSupported = localSupport; compressedBlockAckPeerAddresses = std::set(peerAddresses); } @@ -721,6 +722,17 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat ASSERT(!handler.getAgreement(unlistedPeer, 5)->getIsCompressedBlockAckSupported()); ASSERT(handler.getAgreement(positivePeer, 5)->getIsCompressedBlockAckSupported()); + // Configuration alone must not activate compressed BA in a legacy mode + // or before the active mode set has been supplied. + for (const char *mode : std::initializer_list {nullptr, "a", "b", "g(mixed)", "n(mixed-2.4Ghz)"}) { + localEnabledPolicy.applyModeSet(mode == nullptr ? nullptr : physicallayer::Ieee80211ModeSet::getModeSet(mode)); + bool expected = mode != nullptr && localEnabledPolicy.getModeSet()->isHtOperationSupported(); + ASSERT(localEnabledPolicy.isPeerCompressedBlockAckSupported(listedPeer) == expected); + TestOriginatorBlockAckAgreementHandler modeHandler; + modeHandler.createAgreement(makeAddbaRequest(listedPeer), 1, &localEnabledPolicy); + ASSERT(modeHandler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported() == expected); + } + auto response = makeShared(); response->setTransmitterAddress(listedPeer); response->setBlockAckPolicy(true); From 5bc425b7f4119e73fd24a5d05d4d73f85717ff2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 00:38:42 +0200 Subject: [PATCH 73/91] ieee80211: gate compressed BAR selection on the active mode Established Block Ack agreements retain their compressed-support flag when the mode set changes. Check the current HT capability at request selection so a switch to a legacy mode immediately selects Basic BAR. Exercise the production frame-sequence selector across HT-to-a/b/g transitions with the same agreement, including return to HT and an absent mode set. The transition assertion fails before the fix. The debug build, focused compressed Block Ack unit and runtime module tests, and scoped architecture checks pass. --- .../mac/originator/OriginatorQosAckPolicy.cc | 4 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index bdb9b424b15..db8475cd56c 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -65,7 +65,9 @@ SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const { - return isCompressedBlockAckReqNeeded(outstandingFrames, agreement); + // Agreements retain their negotiated capability across mode-set changes. + // Recheck the active mode whenever selecting a new BAR variant. + return modeSet != nullptr && modeSet->isHtOperationSupported() && isCompressedBlockAckReqNeeded(outstandingFrames, agreement); } bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index c8c5cb7c469..32a611c526a 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -10,10 +10,13 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" @@ -112,6 +115,14 @@ class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy } }; +class OutstandingFramesFixture : public InProgressFrames +{ + public: + std::vector frames; + virtual std::vector getOutstandingFrames() override { return frames; } + virtual ~OutstandingFramesFixture() { for (auto frame : frames) delete frame; } +}; + class TestRecipientQosAckPolicy : public RecipientQosAckPolicy { public: @@ -741,6 +752,39 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat handler.updateAgreement(handler.getAgreement(listedPeer, 5), response, &localEnabledPolicy); ASSERT(handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); EV << "Local and per-peer capability gates are snapshotted by agreements.\n"; + + // Exercise the production BAR selector with the same established agreement + // throughout runtime mode transitions, without refreshing its capability flag. + auto agreement = handler.getActiveAgreement(listedPeer, 5); + ASSERT(agreement != nullptr && agreement->getIsCompressedBlockAckSupported()); + OriginatorQosAckPolicy ackPolicy; + OriginatorBlockAckProcedure procedure; + OutstandingFramesFixture frames; + frames.frames.push_back(makeOutstandingQosFrame(listedPeer, 5)); + for (const char *mode : {"n(mixed-2.4Ghz)", "a", "n(mixed-2.4Ghz)", "b", "n(mixed-2.4Ghz)", "g(mixed)", "n(mixed-2.4Ghz)"}) { + auto modeSet = physicallayer::Ieee80211ModeSet::getModeSet(mode); + localEnabledPolicy.applyModeSet(modeSet); + ackPolicy.applyModeSet(modeSet); + ASSERT(handler.getActiveAgreement(listedPeer, 5) == agreement); + ASSERT(agreement->getIsCompressedBlockAckSupported()); + FrameSequenceContext context(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &frames, nullptr, nullptr, nullptr, + new QoSContext(&ackPolicy, &procedure, &handler, nullptr)); + BlockAckReqBlockAckFs sequence; + sequence.startSequence(&context, 0); + auto step = sequence.prepareStep(&context); + context.addStep(step); + auto packet = check_and_cast(step)->getFrameToTransmit(); + auto details = getOneTidBlockAckReqDetails(packet->peekAtFront()); + ASSERT(details.has_value()); + ASSERT(details->tid == 5); + if (modeSet->isHtOperationSupported()) + ASSERT(dynamicPtrCast(packet->peekAtFront()) != nullptr); + else + ASSERT(dynamicPtrCast(packet->peekAtFront()) != nullptr); + } + ackPolicy.applyModeSet(nullptr); + ASSERT(!ackPolicy.isCompressedBlockAckReq(frames.frames, agreement)); + EV << "Existing HT agreements use Basic BAR immediately after legacy mode transitions.\n"; } { @@ -869,6 +913,7 @@ Compressed bitmap wraps from sequence 4095 to 0. Compressed bitmap preserves leading holes across sequence wrap. Recipient fragment and agreement-policy gates passed. Local and per-peer capability gates are snapshotted by agreements. +Existing HT agreements use Basic BAR immediately after legacy mode transitions. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. From f640fe22f50d866900f81c88c0a7b65561b74129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 01:29:14 +0200 Subject: [PATCH 74/91] ieee80211: limit Basic BAR acknowledgment transitions to 64 positions When a Basic BAR preceded sparse frames, processTransmittedBlockAckReq marked every frame with a sequence number at or above the starting sequence number as awaiting its response. Because processReceivedBlockAck and timeout handling update only the 64-sequence-number window of the Basic Block Ack bitmap, frames beyond offset 63 were never updated, becoming neither retransmittable nor eligible for subsequent requests. Limit the acknowledgment status transition for transmitted Basic BARs to the 64-sequence-number window using BlockAckWindow::isWithin, matching the Basic BlockAck bitmap coverage. Add unit and module regressions for sparse and wrapping sequence sets under Basic BAR, verifying that out-of-window frames remain outstanding and can be requested by subsequent BAR transmissions. Apply the active mode set to originator policy checks in the validation test. Validation: debug and release builds, focused unit and module tests, and scoped architecture, naming, and source-seal gates pass. --- .../ieee80211/mac/originator/QosAckHandler.cc | 3 +- ...Ieee80211CompressedBlockAckValidation.test | 53 ++++++++++++++++++- tests/unit/Ieee80211CompressedBlockAck_1.test | 52 ++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 4cbff5f7074..b28fa8d8f0c 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -222,7 +222,8 @@ void QosAckHandler::processTransmittedBlockAckReq(const Ptr(blockAckReq)) { if (receiverAddress == blockAckReq->getReceiverAddress() && basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum) + // IEEE Std 802.11-2024, 10.25.6.1: the Basic bitmap covers 64 sequence numbers. + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && BlockAckWindow::isWithin(startingSeqNum, 64, SequenceNumberCyclic(seqCtrlField.getSequenceNumber()))) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/tests/module/Ieee80211CompressedBlockAckValidation.test b/tests/module/Ieee80211CompressedBlockAckValidation.test index f48145f2844..a136a9fb4e1 100644 --- a/tests/module/Ieee80211CompressedBlockAckValidation.test +++ b/tests/module/Ieee80211CompressedBlockAckValidation.test @@ -166,6 +166,7 @@ class CompressedBlockAckValidation : public SimpleModule agreement.setIsAddbaResponseReceived(true); agreement.setIsCompressedBlockAckSupported(true); OriginatorQosAckPolicy policy; + policy.applyModeSet(physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); QosAckHandler handler; std::vector frames; for (int offset : {0, 63, 64, 100}) { @@ -213,6 +214,51 @@ class CompressedBlockAckValidation : public SimpleModule delete frame; } + void checkSparseBasic(int start, bool timeout) { + QosAckHandler handler; + std::vector frames; + for (int offset : {0, 63, 64, 100}) { + auto header = dataHeader((start + offset) % 4096); + frames.push_back(new Packet("outstanding", header)); + handler.frameGotInProgress(header); + handler.processTransmittedDataOrMgmtFrame(header); + } + auto bar = makeShared(); + bar->setReceiverAddress(LOCAL); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(start)); + handler.processTransmittedBlockAckReq(bar); + for (int i = 0; i < 4; i++) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == + (i < 2 ? QosAckHandler::Status::WAITING_FOR_BLOCK_ACK : QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED)); + if (timeout) + handler.processFailedBlockAckReq(bar); + else { + auto ba = makeShared(); + ba->setTransmitterAddress(LOCAL); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(start)); + for (int i = 0; i < 64; i++) + ba->setBlockAckBitmap(i, BitVector(std::vector(2, 255))); + ASSERT(handler.processReceivedBlockAck(ba).size() == 2); + } + for (int i = 0; i < 4; i++) { + auto header = frames[i]->peekAtFront(); + ASSERT(handler.getQoSDataAckStatus(header) == (i >= 2 ? QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED : + timeout ? QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED : QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED)); + if (i >= 2) + ASSERT(handler.isOutstandingFrame(header)); + } + // A later BAR can request both residual frames. + auto nextBar = makeShared(*bar); + nextBar->setStartingSequenceNumber(SequenceNumberCyclic((start + 64) % 4096)); + handler.processTransmittedBlockAckReq(nextBar); + for (int i : {2, 3}) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + for (auto frame : frames) + delete frame; + } + virtual void handleMessage(cMessage *message) override { auto savedDeadline = lifetimeAgreement->getExpirationTime(); ASSERT(savedDeadline > simTime()); @@ -272,9 +318,12 @@ class CompressedBlockAckValidation : public SimpleModule checkRecipient(true, false, true, 0); checkRecipient(true, true, false, 0); checkRecipient(false, true, true, 0); - for (int start : {0, 4090}) - for (bool timeout : {false, true}) + for (int start : {0, 4090}) { + for (bool timeout : {false, true}) { checkSparse(start, timeout); + checkSparseBasic(start, timeout); + } + } std::cout << "Compressed BAR validation preserves receive state and limits transmit state to 64 positions.\n"; } }; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 32a611c526a..182118732dd 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -884,6 +884,57 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat EV << "Basic and Compressed BAR rate selection is variant-agnostic.\n"; } +{ + // Sparse Basic Block Ack request limits transmitted-BAR transitions to the 64-sequence window. + QosAckHandler handler; + MacAddress receiver("10:20:30:40:50:60"); + std::vector> headers; + for (int offset : {0, 63, 64, 100}) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiver); + header->setTid(5); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic((4090 + offset) % 4096)); + header->setFragmentNumber(0); + headers.push_back(header); + handler.frameGotInProgress(header); + handler.processTransmittedDataOrMgmtFrame(header); + } + auto basicBar = makeShared(); + basicBar->setReceiverAddress(receiver); + basicBar->setTidInfo(5); + basicBar->setStartingSequenceNumber(SequenceNumberCyclic(4090)); + handler.processTransmittedBlockAckReq(basicBar); + + ASSERT(handler.getQoSDataAckStatus(headers[0]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(handler.getQoSDataAckStatus(headers[1]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(handler.getQoSDataAckStatus(headers[2]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.getQoSDataAckStatus(headers[3]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.isOutstandingFrame(headers[2])); + ASSERT(handler.isOutstandingFrame(headers[3])); + + auto basicBlockAck = makeShared(); + basicBlockAck->setTransmitterAddress(receiver); + basicBlockAck->setTidInfo(5); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(4090)); + for (int i = 0; i < 64; i++) + basicBlockAck->setBlockAckBitmap(i, BitVector(std::vector(2, 0xFF))); + auto acked = handler.processReceivedBlockAck(basicBlockAck); + ASSERT(acked.size() == 2); + ASSERT(handler.getQoSDataAckStatus(headers[0]) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED); + ASSERT(handler.getQoSDataAckStatus(headers[1]) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED); + ASSERT(handler.getQoSDataAckStatus(headers[2]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.getQoSDataAckStatus(headers[3]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + + auto nextBar = makeShared(*basicBar); + nextBar->setStartingSequenceNumber(SequenceNumberCyclic((4090 + 64) % 4096)); + handler.processTransmittedBlockAckReq(nextBar); + ASSERT(handler.getQoSDataAckStatus(headers[2]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(handler.getQoSDataAckStatus(headers[3]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + EV << "Sparse Basic BAR limits waiting statuses to 64 positions across sequence wrap.\n"; +} + EV << ".\n"; %contains: stdout @@ -918,4 +969,5 @@ Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. Basic and Compressed BAR rate selection is variant-agnostic. +Sparse Basic BAR limits waiting statuses to 64 positions across sequence wrap. . From 947f1af537f818c890a659a7d3410a39e566180d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 21 Sep 2026 15:30:11 +0200 Subject: [PATCH 75/91] ieee80211: adapt compressed Block Ack to upstream contracts --- .../mac/Ieee80211MacHeaderSerializer.cc | 18 ------------------ .../RecipientBlockAckAgreementPolicy.cc | 2 +- .../RecipientBlockAckAgreementPolicy.h | 4 ++-- .../RecipientBlockAckAgreementPolicy.ned | 1 + tests/module/Ieee80211HcfAddbaRedDrop_1.test | 2 +- tests/unit/Ieee80211BlockAckActionWire_1.test | 6 +++--- 6 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index 218b9f79e3a..7c63f000c2c 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -90,24 +90,6 @@ void readSequenceControl(MemoryInputStream& stream, int& fragmentNumber, ieee802 sequenceNumber = ieee80211::SequenceNumberCyclic((sequenceControl >> 4) & 0xFFF); } -uint16_t packBlockAckControl(bool ackPolicy, bool multiTid, bool compressedBitmap, uint16_t reserved, uint8_t tidInfo) -{ - return (ackPolicy ? 0x0001 : 0) | - (multiTid ? 0x0002 : 0) | - (compressedBitmap ? 0x0004 : 0) | - ((reserved & 0x1FF) << 3) | - ((tidInfo & 0xF) << 12); -} - -void unpackBlockAckControl(uint16_t control, bool& ackPolicy, bool& multiTid, bool& compressedBitmap, uint16_t& reserved, uint8_t& tidInfo) -{ - ackPolicy = (control & 0x0001) != 0; - multiTid = (control & 0x0002) != 0; - compressedBitmap = (control & 0x0004) != 0; - reserved = (control >> 3) & 0x1FF; - tidInfo = (control >> 12) & 0xF; -} - } // namespace namespace ieee80211 { diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index 062c5c519f4..42a37c293b2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc @@ -14,7 +14,7 @@ Define_Module(RecipientBlockAckAgreementPolicy); void RecipientBlockAckAgreementPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { localCompressedBlockAckSupported = par("localCompressedBlockAckSupported"); for (const auto& address : cStringTokenizer(par("compressedBlockAckPeerAddresses")).asVector()) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h index 16b4da33f8e..d6abedf06fe 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h @@ -9,13 +9,13 @@ #define __INET_RECIPIENTBLOCKACKAGREEMENTPOLICY_H #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" -#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { namespace ieee80211 { -class INET_API RecipientBlockAckAgreementPolicy : public ModeSetListener, public IRecipientBlockAckAgreementPolicy +class INET_API RecipientBlockAckAgreementPolicy : public ModeSetModuleBase, public IRecipientBlockAckAgreementPolicy { protected: int maximumAllowedBufferSize = -1; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned index ff84122b1d3..5d2e931ab3d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned @@ -16,6 +16,7 @@ import inet.linklayer.ieee80211.mac.contract.IRecipientBlockAckAgreementPolicy; simple RecipientBlockAckAgreementPolicy extends SimpleModule like IRecipientBlockAckAgreementPolicy { parameters: + string modeSetModule = default(""); // Required catalog provider; supplied by the enclosing MAC @class(RecipientBlockAckAgreementPolicy); bool localCompressedBlockAckSupported = default(false); // Requires HT operation in the active mode set string compressedBlockAckPeerAddresses = default(""); // Configured peer capability input, snapshotted at ADDBA acceptance diff --git a/tests/module/Ieee80211HcfAddbaRedDrop_1.test b/tests/module/Ieee80211HcfAddbaRedDrop_1.test index 005359b36b0..160d1f7d91f 100644 --- a/tests/module/Ieee80211HcfAddbaRedDrop_1.test +++ b/tests/module/Ieee80211HcfAddbaRedDrop_1.test @@ -17,7 +17,7 @@ class SeedOriginatorHandler : public OriginatorBlockAckAgreementHandler Packet *request(MacAddress peer, Tid tid, IOriginatorBlockAckAgreementPolicy *policy) { auto header = buildAddbaRequest(peer, tid, SequenceNumberCyclic(1), policy); auto id = nextTransactionId++; - createAgreement(header, id); + createAgreement(header, id, policy); auto packet = new Packet("ADDBA", header); packet->addTag()->setTransactionId(id); return packet; diff --git a/tests/unit/Ieee80211BlockAckActionWire_1.test b/tests/unit/Ieee80211BlockAckActionWire_1.test index 737be23e537..9cf18378c7e 100644 --- a/tests/unit/Ieee80211BlockAckActionWire_1.test +++ b/tests/unit/Ieee80211BlockAckActionWire_1.test @@ -18,7 +18,7 @@ static std::vector actionBytes(const std::vector& body) 0x02, 0, 0, 0, 0, 1, 0x02, 0, 0, 0, 0, 2, 0x02, 0, 0, 0, 0, 3, - 0x37, 0x12 + 0x30, 0x12 }; bytes.insert(bytes.end(), body.begin(), body.end()); return bytes; @@ -32,7 +32,7 @@ static Ptr makeAction() header->setReceiverAddress(MacAddress("02:00:00:00:00:01")); header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); header->setAddress3(MacAddress("02:00:00:00:00:03")); - header->setFragmentNumber(7); + header->setFragmentNumber(0); header->setSequenceNumber(SequenceNumberCyclic(0x123)); return header; } @@ -54,7 +54,7 @@ static Ptr checkAction(const Ptr& header, const std::vector ASSERT(parsed->getReceiverAddress() == header->getReceiverAddress()); ASSERT(parsed->getTransmitterAddress() == header->getTransmitterAddress()); ASSERT(parsed->getAddress3() == header->getAddress3()); - ASSERT(parsed->getFragmentNumber() == 7); + ASSERT(parsed->getFragmentNumber() == 0); ASSERT(parsed->getSequenceNumber() == SequenceNumberCyclic(0x123)); ASSERT(parsed->getCategory() == 3); ASSERT(parsed->getBlockAckAction() == header->getBlockAckAction()); From ea6af4e4a112c7b054067a50fe220fc666123504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 11:17:16 +0200 Subject: [PATCH 76/91] Ieee80211: add support for HT primary and secondary channels (HT40- and HT40+) - Added primary, secondary, and bonded center frequency calculations in Ieee80211Channel with HT40+ and HT40- offsets per IEEE Std 802.11-2024 Table 9-134. - Implemented multi-threshold HT CCA sensing (-82 dBm HT20, -79 dBm HT40 bonded, -62 dBm energy detection) in Ieee80211Receiver per Clause 19.3.19.6. - Added IIeee80211CcaProvider interface and ccaStateChangedSignal in Ieee80211Radio for subchannel busy notification. - Implemented secondary channel DIFS idle sensing and EDCA channel access restart in Hcf and Edcaf per Clause 11.15.9. - Added unit tests for channel geometry and CCA sensitivities, and example scenario demonstrating HT20, HT40+, HT40-, and secondary interferer. --- .../channelwidths/ChannelWidthsNetwork.ned | 37 ++++ examples/wireless/channelwidths/omnetpp.ini | 71 ++++++++ .../ieee80211/Ieee80211Interface.ned | 3 + .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 10 +- src/inet/linklayer/ieee80211/mac/Rx.cc | 26 ++- src/inet/linklayer/ieee80211/mac/Rx.h | 6 + .../ieee80211/mac/channelaccess/Dcaf.cc | 11 ++ .../ieee80211/mac/channelaccess/Dcaf.h | 1 + .../ieee80211/mac/channelaccess/Edcaf.cc | 11 ++ .../ieee80211/mac/channelaccess/Edcaf.h | 1 + .../linklayer/ieee80211/mac/contract/IRx.h | 3 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 4 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 24 +++ .../ieee80211/mac/coordinationfunction/Hcf.h | 1 + .../linklayer/ieee80211/mib/Ieee80211Mib.ned | 4 +- .../IsotropicDimensionalBackgroundNoise.cc | 13 +- .../IsotropicScalarBackgroundNoise.cc | 14 +- .../contract/IIeee80211CcaProvider.h | 48 ++++++ .../ieee80211/mode/Ieee80211Channel.cc | 89 +++++++++- .../ieee80211/mode/Ieee80211Channel.h | 16 ++ .../ieee80211/mode/Ieee80211Channel.msg | 1 + .../ieee80211/packetlevel/Ieee80211Radio.cc | 162 +++++++++++++++--- .../ieee80211/packetlevel/Ieee80211Radio.h | 15 +- .../ieee80211/packetlevel/Ieee80211Radio.ned | 4 +- .../packetlevel/Ieee80211Receiver.cc | 124 +++++++++++++- .../ieee80211/packetlevel/Ieee80211Receiver.h | 10 ++ .../packetlevel/Ieee80211Receiver.ned | 5 +- .../packetlevel/Ieee80211Transmitter.cc | 21 ++- .../unit/Ieee80211Ht40SecondaryChannel_1.test | 92 ++++++++++ tests/unit/Ieee80211HtCcaSensitivity_1.test | 51 ++++++ 30 files changed, 815 insertions(+), 63 deletions(-) create mode 100644 examples/wireless/channelwidths/ChannelWidthsNetwork.ned create mode 100644 examples/wireless/channelwidths/omnetpp.ini create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211CcaProvider.h create mode 100644 tests/unit/Ieee80211Ht40SecondaryChannel_1.test create mode 100644 tests/unit/Ieee80211HtCcaSensitivity_1.test diff --git a/examples/wireless/channelwidths/ChannelWidthsNetwork.ned b/examples/wireless/channelwidths/ChannelWidthsNetwork.ned new file mode 100644 index 00000000000..e628831320f --- /dev/null +++ b/examples/wireless/channelwidths/ChannelWidthsNetwork.ned @@ -0,0 +1,37 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +package inet.examples.wireless.channelwidths; + +import inet.networks.base.WirelessNetworkBase; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; + +network ChannelWidthsNetwork extends WirelessNetworkBase +{ + parameters: + @display("bgb=650,450"); + submodules: + ap: AccessPoint { + @display("p=250,200;r=,,#707070"); + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + } + sta1: WirelessHost { + @display("p=400,150;r=,,#707070"); + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + sta2: WirelessHost { + @display("p=400,250;r=,,#707070"); + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + interferer: WirelessHost { + @display("p=400,350;r=,,#707070"); + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } +} diff --git a/examples/wireless/channelwidths/omnetpp.ini b/examples/wireless/channelwidths/omnetpp.ini new file mode 100644 index 00000000000..c266d558c34 --- /dev/null +++ b/examples/wireless/channelwidths/omnetpp.ini @@ -0,0 +1,71 @@ +[General] +network = ChannelWidthsNetwork +sim-time-limit = 2s + +# AP and STA configuration +**.ap.wlan[*].address = "10:00:00:00:00:01" +**.sta1.wlan[*].mgmt.accessPointAddress = "10:00:00:00:00:01" +**.sta2.wlan[*].mgmt.accessPointAddress = "10:00:00:00:00:01" +**.interferer.wlan[*].mgmt.accessPointAddress = "10:00:00:00:00:01" + +# Radio medium parameters +**.radioMedium.backgroundNoise.power = -110dBm +**.radioMedium.analogModel.ignorePartialInterference = true + +# Wi-Fi physical parameters +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].radio.transmitter.power = 20mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.energyDetection = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB + +# Application: UDP traffic between STA1 and STA2 +*.sta1.numApps = 1 +*.sta1.app[0].typename = "UdpBasicApp" +*.sta1.app[0].destAddresses = "sta2" +*.sta1.app[0].destPort = 5000 +*.sta1.app[0].messageLength = 1000B +*.sta1.app[0].sendInterval = 1ms +*.sta1.app[0].startTime = 0.1s +*.sta1.app[0].stopTime = 1.9s + +*.sta2.numApps = 1 +*.sta2.app[0].typename = "UdpSink" +*.sta2.app[0].localPort = 5000 + +# Base HT20 config +[Config Ht20MHz] +description = "IEEE 802.11n 20 MHz operation on 2.4 GHz channel 0" +**.wlan[*].radio.bandwidth = 20MHz +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].htSecondaryChannelOffset = "none" + +# HT40+ (secondary above) +[Config Ht40MHzSecondaryAbove] +description = "IEEE 802.11n 40 MHz operation with secondary channel above (+20 MHz)" +**.wlan[*].radio.bandwidth = 40MHz +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].htSecondaryChannelOffset = "above" + +# HT40- (secondary below) +[Config Ht40MHzSecondaryBelow] +description = "IEEE 802.11n 40 MHz operation with secondary channel below (-20 MHz)" +**.wlan[*].radio.bandwidth = 40MHz +**.wlan[*].radio.channelNumber = 4 +**.wlan[*].htSecondaryChannelOffset = "below" + +# HT40+ with interferer on secondary channel +[Config Ht40MHzSecondaryAboveWithInterferer] +description = "HT40+ with secondary channel interferer causing EDCA backoff restart" +extends = Ht40MHzSecondaryAbove +*.interferer.numApps = 1 +*.interferer.app[0].typename = "UdpBasicApp" +*.interferer.app[0].destAddresses = "sta2" +*.interferer.app[0].destPort = 6000 +*.interferer.app[0].messageLength = 500B +*.interferer.app[0].sendInterval = 2ms +*.interferer.app[0].startTime = 0.2s +*.interferer.app[0].stopTime = 1.8s +*.interferer.wlan[*].radio.bandwidth = 20MHz +*.interferer.wlan[*].radio.channelNumber = 4 +*.interferer.wlan[*].htSecondaryChannelOffset = "none" diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned index bb8e516ab86..a93f017d95f 100644 --- a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned @@ -64,6 +64,7 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface string interfaceTableModule; string energySourceModule = default(""); string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); + string htSecondaryChannelOffset @enum("none", "above", "below") = default("none"); string address @mutable = default("auto"); // MAC address as hex string (12 hex digits), or // "auto". "auto" values will be replaced by // a generated MAC address in init stage 0. @@ -71,6 +72,8 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface double bitrate @unit(bps) = default(-1bps); **.opMode = this.opMode; **.bitrate = this.bitrate; + radio.htSecondaryChannelOffset = default(this.htSecondaryChannelOffset); + mib.htSecondaryChannelOffset = default(this.htSecondaryChannelOffset == "above" ? 1 : this.htSecondaryChannelOffset == "below" ? 3 : 0); mac.modeSet = default(this.opMode); mac.*.rateSelection.dataFrameBitrate = default(this.bitrate); radio.modeSetCoordinatorModule = default(absPath(".mac")); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index 3741992872e..d7793249edb 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -28,6 +28,7 @@ #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/contract/IIeee80211CcaProvider.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" @@ -75,10 +76,13 @@ void Ieee80211Mac::initialize(int stage) rx = check_and_cast(getSubmodule("rx")); tx = check_and_cast(getSubmodule("tx")); prepareLocalCapabilities(); + auto ccaProvider = dynamic_cast(radio.get()); + if (ccaProvider != nullptr) { + radioModule->subscribe(IIeee80211CcaProvider::ccaStateChangedSignal, this); + rx->ccaStateChanged(ccaProvider->getCcaSnapshot()); + } if (isUp()) initializeRadioMode(); - rx = check_and_cast(getSubmodule("rx")); - tx = check_and_cast(getSubmodule("tx")); dcf = check_and_cast(getSubmodule("dcf")); hcf = check_and_cast_nullable(getSubmodule("hcf")); if (mib->qos && !hcf) @@ -463,6 +467,8 @@ void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, cObje { Enter_Method("%s", cComponent::getSignalName(signalID)); // Mode-set application uses the coordinator contract, not notifications. + if (signalID == IIeee80211CcaProvider::ccaStateChangedSignal) + rx->ccaStateChanged(*check_and_cast(obj)); } void Ieee80211Mac::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index 19b9da0889c..beb37d48082 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -159,13 +159,37 @@ void Rx::recomputeMediumFree() { bool oldMediumFree = mediumFree; // note: the duration of mode switching (rx-to-tx or tx-to-rx) should also count as busy - mediumFree = receptionState == IRadio::RECEPTION_STATE_IDLE && transmissionState == IRadio::TRANSMISSION_STATE_UNDEFINED && !endNavTimer->isScheduled(); + bool primaryPhysicallyIdle = ht40Cca ? !primaryCcaBusy : receptionState == IRadio::RECEPTION_STATE_IDLE; + mediumFree = primaryPhysicallyIdle && transmissionState == IRadio::TRANSMISSION_STATE_UNDEFINED && !endNavTimer->isScheduled(); if (mediumFree != oldMediumFree) { for (auto contention : contentions) contention->mediumStateChanged(mediumFree); } } +bool Rx::isSecondaryChannelIdleFor(simtime_t interval) const +{ + return !ht40Cca || (!secondaryCcaBusy && secondaryCcaIdleSince >= SIMTIME_ZERO && + simTime() - secondaryCcaIdleSince >= interval); +} + +void Rx::ccaStateChanged(const Ieee80211CcaSnapshot& snapshot) +{ + Enter_Method("ccaStateChanged"); + bool wasHt40Cca = ht40Cca; + bool wasSecondaryBusy = secondaryCcaBusy; + ht40Cca = snapshot.isHt40(); + primaryCcaBusy = snapshot.isPrimaryBusy(); + secondaryCcaBusy = snapshot.isSecondaryBusy(); + if (!ht40Cca) + secondaryCcaIdleSince = -1; + else if (!wasHt40Cca || (wasSecondaryBusy && !secondaryCcaBusy)) + secondaryCcaIdleSince = simTime(); + else if (secondaryCcaBusy) + secondaryCcaIdleSince = -1; + recomputeMediumFree(); +} + void Rx::receptionStateChanged(IRadio::ReceptionState state) { Enter_Method("receptionStateChanged"); diff --git a/src/inet/linklayer/ieee80211/mac/Rx.h b/src/inet/linklayer/ieee80211/mac/Rx.h index 0fb3fbf81b3..0fe1a8bd964 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.h +++ b/src/inet/linklayer/ieee80211/mac/Rx.h @@ -37,6 +37,10 @@ class INET_API Rx : public SimpleModule, public IRx physicallayer::IRadio::TransmissionState transmissionState = physicallayer::IRadio::TRANSMISSION_STATE_UNDEFINED; physicallayer::IRadioSignal::SignalPart receivedPart = physicallayer::IRadioSignal::SIGNAL_PART_NONE; bool mediumFree = true; // cached state + bool ht40Cca = false; + bool primaryCcaBusy = false; + bool secondaryCcaBusy = false; + simtime_t secondaryCcaIdleSince = -1; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } @@ -53,7 +57,9 @@ class INET_API Rx : public SimpleModule, public IRx virtual bool isReceptionInProgress() const override; virtual bool isMediumFree() const override { return mediumFree; } + virtual bool isSecondaryChannelIdleFor(simtime_t interval) const override; virtual void receptionStateChanged(physicallayer::IRadio::ReceptionState newReceptionState) override; + virtual void ccaStateChanged(const physicallayer::Ieee80211CcaSnapshot& snapshot) override; virtual void transmissionStateChanged(physicallayer::IRadio::TransmissionState transmissionState) override; virtual void receivedSignalPartChanged(physicallayer::IRadioSignal::SignalPart part) override; virtual bool lowerFrameReceived(Packet *packet) override; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 5b3c93abf82..a856998fede 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -103,6 +103,17 @@ void Dcaf::releaseChannel(IChannelAccess::ICallback *callback) EV_INFO << "Channel released.\n"; } +void Dcaf::restartChannelAccess(IChannelAccess::ICallback *callback) +{ + Enter_Method("restartChannelAccess"); + ASSERT(owning && this->callback == callback); + owning = false; + emit(channelOwnershipChangedSignal, owning); + // IEEE Std 802.11-2024, 11.15.9 item b): retain the current CW and draw a + // fresh random backoff without updating any retry counter. + contention->startContention(cw, ifs, eifs, slotTime, this); +} + void Dcaf::requestChannel(IChannelAccess::ICallback *callback) { Enter_Method("requestChannel"); diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h index 2ff03b4eb25..0e87ecd04f5 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h @@ -53,6 +53,7 @@ class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, publ // IChannelAccess::ICallback virtual void requestChannel(IChannelAccess::ICallback *callback) override; virtual void releaseChannel(IChannelAccess::ICallback *callback) override; + virtual void restartChannelAccess(IChannelAccess::ICallback *callback); // IContention::ICallback virtual void channelAccessGranted() override; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index fad741bbc8d..81bf73bc1e8 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -144,6 +144,17 @@ void Edcaf::releaseChannel(IChannelAccess::ICallback *callback) EV_INFO << "Channel released.\n"; } +void Edcaf::restartChannelAccess(IChannelAccess::ICallback *callback) +{ + Enter_Method("restartChannelAccess"); + ASSERT(owning); + owning = false; + emit(channelOwnershipChangedSignal, owning); + // IEEE Std 802.11-2024, 11.15.9 item b): retain CW[AC], select a new + // random backoff, and leave retry counters and queued frames unchanged. + contention->startContention(cw, ifs, eifs, slotTime, this); +} + void Edcaf::requestChannel(IChannelAccess::ICallback *callback) { Enter_Method("requestChannel"); diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index 2a5f6262398..d93ba940689 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h @@ -88,6 +88,7 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub // IChannelAccess virtual void requestChannel(IChannelAccess::ICallback *callback) override; virtual void releaseChannel(IChannelAccess::ICallback *callback) override; + virtual void restartChannelAccess(IChannelAccess::ICallback *callback); // IContention::ICallback virtual void channelAccessGranted() override; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRx.h b/src/inet/linklayer/ieee80211/mac/contract/IRx.h index e930dfc821e..fd7aacb74d5 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRx.h @@ -12,6 +12,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/contract/IContention.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/ieee80211/contract/IIeee80211CcaProvider.h" namespace inet { namespace ieee80211 { @@ -35,6 +36,7 @@ class INET_API IRx // from Contention virtual bool isMediumFree() const = 0; + virtual bool isSecondaryChannelIdleFor(simtime_t interval) const = 0; virtual void frameTransmitted(simtime_t durationField) = 0; // from Coordination functions @@ -42,6 +44,7 @@ class INET_API IRx // events virtual void receptionStateChanged(physicallayer::IRadio::ReceptionState state) = 0; + virtual void ccaStateChanged(const physicallayer::Ieee80211CcaSnapshot& snapshot) = 0; virtual void transmissionStateChanged(physicallayer::IRadio::TransmissionState state) = 0; virtual void receivedSignalPartChanged(physicallayer::IRadioSignal::SignalPart part) = 0; virtual bool lowerFrameReceived(Packet *packet) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 65bfef59d83..e65618e3fc2 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -18,6 +18,7 @@ #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" namespace inet { namespace ieee80211 { @@ -345,7 +346,8 @@ void Dcf::transmitFrame(Packet *packet, simtime_t ifs) { Enter_Method("transmitFrame"); const auto& header = packet->peekAtFront(); - auto mode = rateSelection->computeMode(packet, header); + auto modeReq = packet->findTag(); + auto mode = modeReq != nullptr ? modeReq->getMode() : rateSelection->computeMode(packet, header); RateSelection::setFrameMode(packet, header, mode); RateSelection::emitDatarateSelected(this, header, mode); EV_DEBUG << "Datarate for " << packet->getName() << " is set to " << mode->getDataMode()->getNetBitrate() << ".\n"; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 17b61a887ab..0143cbab978 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -27,6 +27,7 @@ #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" namespace inet { @@ -648,6 +649,24 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& } } +bool Hcf::shouldRestartHt40ChannelAccess(Edcaf *edcaf) +{ + Packet *frame = edcaf->getInProgressFrames()->getFrameToTransmit(); + if (frame == nullptr) + return false; + const auto& header = frame->peekAtFront(); + auto modeReq = frame->findTag(); + auto mode = modeReq != nullptr ? modeReq->getMode() : rateSelection->computeMode(frame, header, edcaf->getTxopProcedure()); + if (mode == nullptr || dynamic_cast(mode) == nullptr || + mode->getDataMode()->getBandwidth() != MHz(40)) + return false; + // IEEE Std 802.11-2024, 11.15.9 item b): + // If the secondary channel was busy during DIFS before channel access, + // invoke the backoff procedure with the current CW[AC]. + simtime_t difs = modeSet->getSifsTime() + 2 * modeSet->getSlotTime(); + return !rx->isSecondaryChannelIdleFor(difs); +} + void Hcf::channelGranted(IChannelAccess *channelAccess) { Enter_Method("channelGranted"); @@ -655,6 +674,11 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) if (edcaf) { AccessCategory ac = edcaf->getAccessCategory(); EV_DETAIL << "Channel access granted to the " << printAccessCategory(ac) << " queue" << std::endl; + if (shouldRestartHt40ChannelAccess(edcaf)) { + EV_INFO << "Secondary channel was busy during DIFS before channel access for HT40 transmission, restarting backoff.\n"; + edcaf->restartChannelAccess(this); + return; + } auto internallyCollidedEdcafs = edca->getInternallyCollidedEdcafs(); if (internallyCollidedEdcafs.size() > 0) { EV_INFO << "Internal collision happened with the following queues:" << std::endl; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 4faa36a39d8..2f044366875 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -180,6 +180,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void setFrameMode(Packet *packet, const Ptr& header, const physicallayer::IIeee80211Mode *mode) const; virtual bool isSentByUs(const Ptr& header) const; virtual bool isForUs(const Ptr& header) const; + virtual bool shouldRestartHt40ChannelAccess(Edcaf *edcaf); protected: // IFrameSequenceHandler::ICallback diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index 138515cfa0a..b16cb930b75 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -27,8 +27,10 @@ simple Ieee80211Mib extends SimpleModule @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; requires PHY width support. The packet-level Ieee80211Transmitter/Receiver support only 20 MHz and reject nonzero offsets. + int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; requires PHY width support. int htProtectionMode = default(0); // 0=none, 1=nonmember, 2=20 MHz, 3=non-HT mixed + bool htShortGi40 = default(false); + int htMaxMcs = default(7); displayStringTextFormat = default("Address: {address}{ssidStr}\n{modeStr}{stationTypeStr}{qosStr}{associatedStr}"); @display("i=block/table"); } diff --git a/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicDimensionalBackgroundNoise.cc b/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicDimensionalBackgroundNoise.cc index c6edffd46df..e605630ea30 100644 --- a/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicDimensionalBackgroundNoise.cc +++ b/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicDimensionalBackgroundNoise.cc @@ -54,17 +54,16 @@ const INoise *IsotropicDimensionalBackgroundNoise::computeNoise(const IListening if (!std::isnan(powerSpectralDensity.get())) noisePowerSpectralDensity = powerSpectralDensity; else { - if (std::isnan(bandwidth.get())) - bandwidth = listeningBandwidth; - else if (bandwidth != listeningBandwidth) - throw cRuntimeError("The powerSpectralDensity parameter is not specified and the power parameter cannot be used, because background noise bandwidth doesn't match listening bandwidth"); - // NOTE: dividing by the bandwidth here makes sure the total background noise power in the listening band is the given power - noisePowerSpectralDensity = power / bandwidth; + Hz noiseBandwidth = std::isnan(bandwidth.get()) ? listeningBandwidth : bandwidth; + // A scalar power parameter denotes integrated power over the + // configured band. Keep its equivalent flat PSD when a per-channel + // CCA query selects a narrower HT40 slice. + noisePowerSpectralDensity = power / noiseBandwidth; } const Ptr>>& powerFunction = makeShared>>(noisePowerSpectralDensity); const simtime_t startTime = listening->getStartTime(); const simtime_t endTime = listening->getEndTime(); - return new DimensionalNoise(startTime, endTime, centerFrequency, bandwidth, makeFirstQuadrantLimitedFunction(powerFunction)); + return new DimensionalNoise(startTime, endTime, centerFrequency, listeningBandwidth, makeFirstQuadrantLimitedFunction(powerFunction)); } } // namespace physicallayer diff --git a/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicScalarBackgroundNoise.cc b/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicScalarBackgroundNoise.cc index 4c250aab604..39f13c920bf 100644 --- a/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicScalarBackgroundNoise.cc +++ b/src/inet/physicallayer/wireless/common/backgroundnoise/IsotropicScalarBackgroundNoise.cc @@ -43,12 +43,14 @@ const INoise *IsotropicScalarBackgroundNoise::computeNoise(const IListening *lis simtime_t endTime = listening->getEndTime(); Hz centerFrequency = bandListening->getCenterFrequency(); Hz listeningBandwidth = bandListening->getBandwidth(); - if (std::isnan(bandwidth.get())) - bandwidth = listeningBandwidth; - else if (bandwidth != listeningBandwidth) - throw cRuntimeError("Background noise bandwidth doesn't match listening bandwidth"); - const auto& powerFunction = makeShared>(startTime, endTime, power); - return new ScalarNoise(startTime, endTime, centerFrequency, bandwidth, powerFunction); + Hz noiseBandwidth = std::isnan(bandwidth.get()) ? listeningBandwidth : bandwidth; + // A scalar background-noise power is the integrated power over its + // configured bandwidth. Per-channel HT40 CCA listens to 20 MHz slices, + // so preserve the equivalent flat noise density instead of rejecting a + // narrower listening band. + W listeningPower = power * (listeningBandwidth / noiseBandwidth); + const auto& powerFunction = makeShared>(startTime, endTime, listeningPower); + return new ScalarNoise(startTime, endTime, centerFrequency, listeningBandwidth, powerFunction); } } // namespace physicallayer diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211CcaProvider.h b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211CcaProvider.h new file mode 100644 index 00000000000..77f309bf8c0 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/IIeee80211CcaProvider.h @@ -0,0 +1,48 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IIEEE80211CCAPROVIDER_H +#define __INET_IIEEE80211CCAPROVIDER_H + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +class INET_API Ieee80211CcaSnapshot : public cObject +{ + protected: + bool ht40 = false; + bool primaryBusy = false; + bool secondaryBusy = false; + + public: + Ieee80211CcaSnapshot(bool ht40 = false, bool primaryBusy = false, bool secondaryBusy = false) : + ht40(ht40), primaryBusy(primaryBusy), secondaryBusy(secondaryBusy) {} + + virtual Ieee80211CcaSnapshot *dup() const override { return new Ieee80211CcaSnapshot(*this); } + + bool isHt40() const { return ht40; } + bool isPrimaryBusy() const { return primaryBusy; } + bool isSecondaryBusy() const { return secondaryBusy; } +}; + +/** + * Provides the local IEEE 802.11 per-channel PHY-CCA state. The snapshot is + * local PHY/MAC control information and is not transmitted on air. + */ +class INET_API IIeee80211CcaProvider +{ + public: + static simsignal_t ccaStateChangedSignal; + + virtual ~IIeee80211CcaProvider() {} + + virtual const Ieee80211CcaSnapshot& getCcaSnapshot() const = 0; +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.cc index 640a79c9c06..f9d579126a8 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.cc @@ -12,9 +12,93 @@ namespace inet { namespace physicallayer { Ieee80211Channel::Ieee80211Channel(const IIeee80211Band *band, int channelNumber) : + Ieee80211Channel(band, channelNumber, IEEE80211_SECONDARY_CHANNEL_NONE) +{ +} + +Ieee80211Channel::Ieee80211Channel(const IIeee80211Band *band, int channelNumber, Ieee80211SecondaryChannelOffset secondaryChannelOffset) : band(band), - channelNumber(channelNumber) + channelNumber(channelNumber), + secondaryChannelOffset(secondaryChannelOffset) +{ + // IEEE Std 802.11-2024, Table 9-134 reserves wire value 2; 19.2.3 and + // 19.3.15.4 place the secondary channel four channel numbers away. + if (secondaryChannelOffset != IEEE80211_SECONDARY_CHANNEL_NONE && + secondaryChannelOffset != IEEE80211_SECONDARY_CHANNEL_ABOVE && + secondaryChannelOffset != IEEE80211_SECONDARY_CHANNEL_BELOW) + throw cRuntimeError("Invalid IEEE 802.11 secondary channel offset: %d", (int)secondaryChannelOffset); + if (secondaryChannelOffset != IEEE80211_SECONDARY_CHANNEL_NONE) { + if (band == nullptr) + throw cRuntimeError("Cannot resolve an IEEE 802.11 secondary channel without a band"); + (void)getCenterFrequency(); + (void)getSecondaryCenterFrequency(); + } +} + +Ieee80211SecondaryChannelOffset Ieee80211Channel::parseSecondaryChannelOffset(const char *text) +{ + if (!strcmp(text, "none")) + return IEEE80211_SECONDARY_CHANNEL_NONE; + if (!strcmp(text, "above")) + return IEEE80211_SECONDARY_CHANNEL_ABOVE; + if (!strcmp(text, "below")) + return IEEE80211_SECONDARY_CHANNEL_BELOW; + throw cRuntimeError("Unknown IEEE 802.11 secondary channel offset '%s'", text); +} + +const char *Ieee80211Channel::getSecondaryChannelOffsetName(Ieee80211SecondaryChannelOffset offset) +{ + switch (offset) { + case IEEE80211_SECONDARY_CHANNEL_NONE: + return "none"; + case IEEE80211_SECONDARY_CHANNEL_ABOVE: + return "above"; + case IEEE80211_SECONDARY_CHANNEL_BELOW: + return "below"; + default: + throw cRuntimeError("Unknown IEEE 802.11 secondary channel offset: %d", (int)offset); + } +} + +int Ieee80211Channel::getSecondaryChannelNumber() const +{ + switch (secondaryChannelOffset) { + case IEEE80211_SECONDARY_CHANNEL_NONE: + return channelNumber; + case IEEE80211_SECONDARY_CHANNEL_ABOVE: + return channelNumber + 4; + case IEEE80211_SECONDARY_CHANNEL_BELOW: + return channelNumber - 4; + default: + throw cRuntimeError("Unknown secondary channel offset: %d", secondaryChannelOffset); + } +} + +Hz Ieee80211Channel::getSecondaryCenterFrequency() const +{ + int direction = secondaryChannelOffset == IEEE80211_SECONDARY_CHANNEL_ABOVE ? 1 : + secondaryChannelOffset == IEEE80211_SECONDARY_CHANNEL_BELOW ? -1 : 0; + if (direction == 0) + throw cRuntimeError("IEEE 802.11 channel has no secondary channel"); + int secondaryChannelNumber = channelNumber + 4 * direction; + Hz secondaryCenterFrequency = band->getCenterFrequency(secondaryChannelNumber); + if (secondaryCenterFrequency != getCenterFrequency() + MHz(20 * direction)) + throw cRuntimeError("IEEE 802.11 secondary channel is not 20 MHz from the primary channel"); + return secondaryCenterFrequency; +} + +Hz Ieee80211Channel::getBondedCenterFrequency() const { + switch (secondaryChannelOffset) { + case IEEE80211_SECONDARY_CHANNEL_NONE: + return getCenterFrequency(); + case IEEE80211_SECONDARY_CHANNEL_ABOVE: + return getCenterFrequency() + MHz(10); + case IEEE80211_SECONDARY_CHANNEL_BELOW: + return getCenterFrequency() - MHz(10); + default: + throw cRuntimeError("Invalid IEEE 802.11 secondary channel offset: %d", (int)secondaryChannelOffset); + } } std::ostream& Ieee80211Channel::printToStream(std::ostream& stream, int level, int evFlags) const @@ -23,7 +107,8 @@ std::ostream& Ieee80211Channel::printToStream(std::ostream& stream, int level, i if (level <= PRINT_LEVEL_TRACE) stream << EV_FIELD(band, printFieldToString(band, level + 1, evFlags)); if (level <= PRINT_LEVEL_INFO) - stream << EV_FIELD(channelNumber); + stream << EV_FIELD(channelNumber) + << EV_FIELD(secondaryChannelOffset); return stream; } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h index 1de53f6e67a..0fd0207bffb 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h @@ -14,20 +14,36 @@ namespace inet { namespace physicallayer { +// IEEE Std 802.11-2024, Table 9-134. +enum Ieee80211SecondaryChannelOffset { + IEEE80211_SECONDARY_CHANNEL_NONE = 0, + IEEE80211_SECONDARY_CHANNEL_ABOVE = 1, + IEEE80211_SECONDARY_CHANNEL_BELOW = 3, +}; + class INET_API Ieee80211Channel : public IPrintableObject { protected: const IIeee80211Band *band; int channelNumber; + Ieee80211SecondaryChannelOffset secondaryChannelOffset; public: Ieee80211Channel(const IIeee80211Band *band, int channelNumber); + Ieee80211Channel(const IIeee80211Band *band, int channelNumber, Ieee80211SecondaryChannelOffset secondaryChannelOffset); + + static Ieee80211SecondaryChannelOffset parseSecondaryChannelOffset(const char *text); + static const char *getSecondaryChannelOffsetName(Ieee80211SecondaryChannelOffset offset); virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override; virtual const IIeee80211Band *getBand() const { return band; } virtual int getChannelNumber() const { return channelNumber; } + virtual Ieee80211SecondaryChannelOffset getSecondaryChannelOffset() const { return secondaryChannelOffset; } + virtual int getSecondaryChannelNumber() const; virtual Hz getCenterFrequency() const { return band->getCenterFrequency(channelNumber); } + virtual Hz getSecondaryCenterFrequency() const; + virtual Hz getBondedCenterFrequency() const; }; } // namespace physicallayer diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.msg b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.msg index 08eb8e7e897..491e7a26ac3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.msg +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.msg @@ -40,4 +40,5 @@ class Ieee80211Channel @descriptor(readonly); IIeee80211Band *band; int channelNumber; + int secondaryChannelOffset; } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 5c0ff55e7e9..3d6ae57a94d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -29,6 +29,10 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211OfdmSignalField.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/mobility/contract/IMobility.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/BandListening.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/ListeningDecision.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" @@ -40,12 +44,56 @@ namespace physicallayer { Define_Module(Ieee80211Radio); simsignal_t Ieee80211Radio::radioChannelChangedSignal = cComponent::registerSignal("radioChannelChanged"); +simsignal_t IIeee80211CcaProvider::ccaStateChangedSignal = cComponent::registerSignal("ccaStateChanged"); Ieee80211Radio::Ieee80211Radio() : - FlatRadioBase() + FlatRadioBase(), + ccaSnapshot(std::make_unique()) { } +bool Ieee80211Radio::computeIsBandBusy(Hz centerFrequency) const +{ + const simtime_t now = simTime(); + const Coord& position = antenna->getMobility()->getCurrentPosition(); + BandListening listening(this, now, now + SimTime::fromRaw(1), position, position, + centerFrequency, MHz(20)); + const IListeningDecision *decision = medium->listenOnMedium(this, &listening); + bool busy = decision->isListeningPossible(); + delete decision; + return busy; +} + +void Ieee80211Radio::updateCcaState() +{ + auto ieee80211Receiver = dynamic_cast(receiver); + auto channel = ieee80211Receiver == nullptr ? nullptr : ieee80211Receiver->getChannel(); + bool ht40Configured = channel != nullptr && + channel->getSecondaryChannelOffset() != IEEE80211_SECONDARY_CHANNEL_NONE && + modeSet != nullptr && !strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") && + ieee80211Receiver->getBandwidth() == MHz(40); + bool ht40 = ht40Configured && isReceiverMode(radioMode); + bool primaryBusy = false; + bool secondaryBusy = false; + if (ht40) { + // IEEE Std 802.11-2024, 8.3.5.12/Table 8-5 and 19.3.19.6.5: + // preserve {primary}, {secondary}, and {primary,secondary} CCA state. + primaryBusy = computeIsBandBusy(channel->getCenterFrequency()); + secondaryBusy = computeIsBandBusy(channel->getSecondaryCenterFrequency()); + } + if (ccaSnapshot->isHt40() != ht40 || ccaSnapshot->isPrimaryBusy() != primaryBusy || + ccaSnapshot->isSecondaryBusy() != secondaryBusy) { + ccaSnapshot = std::make_unique(ht40, primaryBusy, secondaryBusy); + emit(IIeee80211CcaProvider::ccaStateChangedSignal, ccaSnapshot.get()); + } +} + +void Ieee80211Radio::updateTransceiverState() +{ + FlatRadioBase::updateTransceiverState(); + updateCcaState(); +} + void Ieee80211Radio::initialize(int stage) { FlatRadioBase::initialize(stage); @@ -54,8 +102,31 @@ void Ieee80211Radio::initialize(int stage) modeSetCoordinator.reference(this, "modeSetCoordinatorModule", false); const char *fcsModeString = par("fcsMode"); fcsMode = parseFcsMode(fcsModeString, true); + opMode = par("opMode").stringValue(); } if (stage == INITSTAGE_PHYSICAL_LAYER) { + const char *bandName = par("bandName"); + setBand(*bandName ? Ieee80211CompliantBands::getBand(bandName) : nullptr); + setModeSet(*opMode.c_str() ? Ieee80211ModeSet::getModeSet(opMode.c_str()) : nullptr); + htSecondaryChannelOffset = Ieee80211Channel::parseSecondaryChannelOffset(par("htSecondaryChannelOffset")); + Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); + Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); + Hz radioBw = !std::isnan(par("bandwidth").doubleValue()) ? Hz(par("bandwidth").doubleValue()) : ieee80211Receiver->getBandwidth(); + if (radioBw == MHz(40)) { + ieee80211Receiver->setBandwidth(radioBw); + ieee80211Transmitter->setBandwidth(radioBw); + } + if (modeSet != nullptr && !strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") && + radioBw == MHz(40) && + htSecondaryChannelOffset == IEEE80211_SECONDARY_CHANNEL_NONE) + throw cRuntimeError("HT 40 MHz operation requires a secondary channel offset of above or below"); + if (htSecondaryChannelOffset != IEEE80211_SECONDARY_CHANNEL_NONE) { + // IEEE Std 802.11-2024, 19.2.3 and 19.3.15.4: the secondary + // channel is an HT40-only operating-channel property. + if (modeSet == nullptr || strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") || + radioBw != MHz(40)) + throw cRuntimeError("htSecondaryChannelOffset above/below requires HT 40 MHz operation"); + } int channelNumber = par("channelNumber"); if (channelNumber != -1) setChannelNumber(channelNumber); @@ -75,28 +146,68 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) if (message->getKind() == RADIO_C_CONFIGURE) { Ieee80211ConfigureRadioCommand *configureCommand = dynamic_cast(message->getControlInfo()); if (configureCommand != nullptr) { - const char *opMode = configureCommand->getOpMode(); - const Ieee80211ModeSet *modeSet = configureCommand->getModeSet(); - // 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 (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) - setBand(band); + Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); + Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); + const Ieee80211Channel *currentChannel = ieee80211Receiver->getChannel(); + const char *requestedOpMode = configureCommand->getOpMode(); + std::string targetOpMode = *requestedOpMode ? requestedOpMode : this->opMode; + const IIeee80211Band *bandParam = configureCommand->getBand(); + const IIeee80211Band *targetBand = bandParam != nullptr ? bandParam : this->band; + const Ieee80211ModeSet *modeSetParam = configureCommand->getModeSet(); + const Ieee80211ModeSet *targetModeSet = modeSetParam != nullptr ? modeSetParam : + *requestedOpMode ? Ieee80211ModeSet::getModeSet(requestedOpMode) : this->modeSet; const Ieee80211Channel *channel = configureCommand->getChannel(); - if (channel != nullptr) - setChannel(channel); int newChannelNumber = configureCommand->getChannelNumber(); - if (newChannelNumber != -1) - setChannelNumber(newChannelNumber); + int targetChannelNumber = channel != nullptr ? channel->getChannelNumber() : + newChannelNumber != -1 ? newChannelNumber : + currentChannel != nullptr ? currentChannel->getChannelNumber() : -1; + auto targetSecondaryChannelOffset = channel != nullptr ? channel->getSecondaryChannelOffset() : + htSecondaryChannelOffset; + if (targetChannelNumber != -1 && (targetBand == nullptr || targetChannelNumber < 0 || targetChannelNumber >= targetBand->getNumChannels())) + throw cRuntimeError("Invalid target 802.11 channel number %d", targetChannelNumber); + if (targetChannelNumber != -1) + (void)Ieee80211Channel(targetBand, targetChannelNumber, targetSecondaryChannelOffset).getCenterFrequency(); + + Hz newBandwidth = configureCommand->getBandwidth(); + Hz targetBandwidth = std::isnan(newBandwidth.get()) ? ieee80211Receiver->getBandwidth() : newBandwidth; + bps newBitrate = configureCommand->getBitrate(); + bps targetBitrate = std::isnan(newBitrate.get()) ? ieee80211Transmitter->getBitrate() : newBitrate; + const IIeee80211Mode *mode = configureCommand->getMode(); + Hz targetModeBandwidth = !std::isnan(targetBandwidth.get()) ? targetBandwidth : Hz(MHz(20)); + const IIeee80211Mode *resolvedMode = mode != nullptr ? mode : nullptr; + if (resolvedMode == nullptr && targetModeSet != nullptr && targetBitrate != bps(-1)) + resolvedMode = targetModeSet->getMode(targetBitrate, targetModeBandwidth); + if (targetModeSet != nullptr && !strcmp(targetModeSet->getName(), "n(mixed-2.4Ghz)") && + ((targetModeBandwidth == MHz(40)) || + (resolvedMode != nullptr && dynamic_cast(resolvedMode) != nullptr && + resolvedMode->getDataMode()->getBandwidth() == MHz(40))) && + targetSecondaryChannelOffset == IEEE80211_SECONDARY_CHANNEL_NONE) + throw cRuntimeError("HT 40 MHz operation requires a secondary channel offset of above or below"); + + bool publishModeSet = targetModeSet != this->modeSet || targetBand != this->band || + !std::isnan(newBandwidth.get()) || !std::isnan(newBitrate.get()) || *requestedOpMode; + + if (!std::isnan(newBandwidth.get())) + setBandwidth(newBandwidth); + if (!std::isnan(newBitrate.get())) + setBitrate(newBitrate); + if (targetChannelNumber != -1 && + (currentChannel == nullptr || targetBand != this->band || targetChannelNumber != currentChannel->getChannelNumber() || + targetSecondaryChannelOffset != currentChannel->getSecondaryChannelOffset())) + setChannel(new Ieee80211Channel(targetBand, targetChannelNumber, targetSecondaryChannelOffset)); + else if (targetBand != this->band) + setBand(targetBand); + this->opMode = targetOpMode; + if (publishModeSet && targetModeSet != nullptr) { + if (resolvedMode != nullptr) + setModeSetAndMode(targetModeSet, resolvedMode); + else + setModeSet(targetModeSet); + } + else if (resolvedMode != nullptr) + setMode(resolvedMode); + delete message; + return; } } FlatRadioBase::handleUpperCommand(message); @@ -184,6 +295,7 @@ void Ieee80211Radio::setMode(const IIeee80211Mode *mode) void Ieee80211Radio::setBand(const IIeee80211Band *band) { + this->band = band; Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); ieee80211Transmitter->setBand(band); @@ -206,7 +318,9 @@ void Ieee80211Radio::setChannel(const Ieee80211Channel *channel) Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); ieee80211Transmitter->setChannel(channel); - ieee80211Receiver->setChannel(new Ieee80211Channel(channel->getBand(), channel->getChannelNumber())); + ieee80211Receiver->setChannel(new Ieee80211Channel(channel->getBand(), channel->getChannelNumber(), channel->getSecondaryChannelOffset())); + band = channel->getBand(); + htSecondaryChannelOffset = channel->getSecondaryChannelOffset(); EV << "Changing radio channel to " << channel->getChannelNumber() << endl; abortIncompatibleReception(); Ieee80211RadioChannelChangedDetails details(channel->getBand()); @@ -219,8 +333,8 @@ void Ieee80211Radio::setChannelNumber(int newChannelNumber) { Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); - ieee80211Transmitter->setChannelNumber(newChannelNumber); - ieee80211Receiver->setChannelNumber(newChannelNumber); + ieee80211Transmitter->setChannel(new Ieee80211Channel(band, newChannelNumber, htSecondaryChannelOffset)); + ieee80211Receiver->setChannel(new Ieee80211Channel(band, newChannelNumber, htSecondaryChannelOffset)); EV << "Changing radio channel to " << newChannelNumber << ".\n"; abortIncompatibleReception(); Ieee80211RadioChannelChangedDetails details(ieee80211Transmitter->getChannel()->getBand()); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 3d526363aea..206d371ecb1 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -12,6 +12,7 @@ #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/contract/IIeee80211CcaProvider.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" @@ -21,7 +22,7 @@ namespace inet { namespace physicallayer { -class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio +class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio, public IIeee80211CcaProvider { public: /** @@ -38,6 +39,11 @@ class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio ModuleRefByPar modeSetCoordinator; bool changingModeSet = false; FcsMode fcsMode = FCS_MODE_UNDEFINED; + Ieee80211SecondaryChannelOffset htSecondaryChannelOffset = IEEE80211_SECONDARY_CHANNEL_NONE; + std::unique_ptr ccaSnapshot; + std::string opMode; + const Ieee80211ModeSet *modeSet = nullptr; + const IIeee80211Band *band = nullptr; protected: virtual void initialize(int stage) override; @@ -53,11 +59,14 @@ class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio virtual void encapsulate(Packet *packet) const override; virtual void decapsulate(Packet *packet) const override; + virtual bool computeIsBandBusy(Hz centerFrequency) const; + virtual void updateCcaState(); + virtual void updateTransceiverState() override; + public: Ieee80211Radio(); - // Update behavioral consumers before publishing the new mode set. - // Failures are fatal simulation errors; these setters do not roll back. +class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio, public IIeee80211CcaProvider // Behavioral consumers implement IIeee80211ModeSetListener. virtual const Ieee80211Channel *getChannel() const override; virtual bool isHtChannelWidthSupported(Hz channelWidth) const override; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned index d4683437183..5997e38b10a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned @@ -31,8 +31,10 @@ module Ieee80211Radio extends FlatRadioBase like IIeee80211Radio string opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)", "p", "ac") = default("g(mixed)"); // Operation mode string bandName @enum("2.4 GHz", "5 GHz", "5 GHz (20 MHz)", "5 GHz (40 MHz)", "5 GHz (80 MHz)", "5 GHz (160 MHz)", "5.9 GHz") = default("2.4 GHz"); // Band name int channelNumber = default(0); // Initial channel number within the band (TODO this is offset by 1) + string htSecondaryChannelOffset @enum("none", "above", "below") = default("none"); // HT40 secondary channel relative to the primary channel string fcsMode @enum("declared","computed") = default("declared"); transmitter.typename = default("Ieee80211Transmitter"); + transmitter.channelNumber = this.channelNumber; receiver.typename = default("Ieee80211Receiver"); receiver.errorModel.typename = default(signalAnalogRepresentation != "unitDisk" ? "Ieee80211NistErrorModel" : ""); centerFrequency @unit(Hz) = default(2.412GHz); // The center frequency of the band is automatically set by the radio @@ -47,8 +49,8 @@ module Ieee80211Radio extends FlatRadioBase like IIeee80211Radio receiver.snirThreshold = default(4dB); *.opMode = this.opMode; *.bandName = this.bandName; - *.channelNumber = this.channelNumber; @class(Ieee80211Radio); @signal[radioChannelChanged](type=long); // Band-local index; Ieee80211RadioChannelChangedDetails carries the band + @signal[ccaStateChanged]; @statistic[radioChannel](title="Radio channel"; source=radioChannelChanged; record=histogram,vector; interpolationmode=sample-hold); } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 9514cdccb28..4dbab31a01b 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -7,6 +7,14 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" +#include "inet/common/math/Functions.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/INarrowbandSignalAnalogModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/BandListening.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/ListeningDecision.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" @@ -26,6 +34,9 @@ void Ieee80211Receiver::initialize(int stage) { FlatReceiverBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { + htCca20Sensitivity = mW(math::dBmW2mW(par("htCca20Sensitivity"))); + htCca40Sensitivity = mW(math::dBmW2mW(par("htCca40Sensitivity"))); + htCcaEnergyDetection = mW(math::dBmW2mW(par("htCcaEnergyDetection"))); const char *opMode = par("opMode"); setModeSet(*opMode ? Ieee80211ModeSet::getModeSet(opMode) : nullptr); const char *bandName = par("bandName"); @@ -59,6 +70,98 @@ bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, return ieee80211Transmission && modeSet->supportsMode(ieee80211Transmission->getMode()) && getAnalogModel()->computeIsReceptionPossible(listening, reception, sensitivity); } +const IListeningDecision *Ieee80211Receiver::computeListeningDecision(const IListening *listening, const IInterference *interference) const +{ + if (isHtCcaOperation() && dynamic_cast(listening) != nullptr && + dynamic_cast(listening)->getBandwidth() == MHz(20)) + return new ListeningDecision(listening, computeHtCcaBusy(listening, interference)); + return FlatReceiverBase::computeListeningDecision(listening, interference); +} + +bool Ieee80211Receiver::isHtCcaOperation() const +{ + return modeSet != nullptr && !strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") && + channel != nullptr && (bandwidth == MHz(20) || + (bandwidth == MHz(40) && channel->getSecondaryChannelOffset() != IEEE80211_SECONDARY_CHANNEL_NONE)); +} + +static bool isBandOverlapping(const BandListening *listening, const INarrowbandSignalAnalogModel *signal) +{ + auto listeningMin = listening->getCenterFrequency() - listening->getBandwidth() / 2; + auto listeningMax = listening->getCenterFrequency() + listening->getBandwidth() / 2; + auto signalMin = signal->getCenterFrequency() - signal->getBandwidth() / 2; + auto signalMax = signal->getCenterFrequency() + signal->getBandwidth() / 2; + return signalMin <= listeningMax && signalMax >= listeningMin; +} + +static bool isPrimaryChannel(const Ieee80211Channel *channel, const BandListening *listening) +{ + return channel != nullptr && listening->getCenterFrequency() == channel->getCenterFrequency(); +} + +static bool isSecondaryChannel(const Ieee80211Channel *channel, const BandListening *listening) +{ + return channel != nullptr && channel->getSecondaryChannelOffset() != IEEE80211_SECONDARY_CHANNEL_NONE && + listening->getCenterFrequency() == channel->getSecondaryCenterFrequency(); +} + +static bool isHt40SignalOccupyingChannel(const Ieee80211Channel *channel, const INarrowbandSignalAnalogModel *signal) +{ + return channel != nullptr && signal->getBandwidth() == MHz(40) && + signal->getCenterFrequency() == channel->getBondedCenterFrequency(); +} + +bool Ieee80211Receiver::computeHtCcaBusy(const IListening *listening, const IInterference *interference) const +{ + const auto *bandListening = check_and_cast(listening); + const auto *mediumAnalogModel = listening->getReceiverRadio()->getMedium()->getAnalogModel(); + const INoise *noise = mediumAnalogModel->computeNoise(listening, interference); + bool busy = noise->computeMaxPower(listening->getStartTime(), listening->getEndTime()) >= htCcaEnergyDetection; + delete noise; + if (busy) + return true; + + const bool primary = isPrimaryChannel(channel, bandListening); + const bool secondary = isSecondaryChannel(channel, bandListening); + if (!primary && !secondary) + return false; + + for (auto reception : *interference->getInterferingReceptions()) { + const auto *transmission = dynamic_cast(reception->getTransmission()); + const auto *signal = dynamic_cast(reception->getAnalogModel()); + if (transmission == nullptr || transmission->getMode() == nullptr || signal == nullptr || + !modeSet->containsMode(transmission->getMode()) || !isBandOverlapping(bandListening, signal)) + continue; + + const W signalPower = signal->computeMinPower(reception->getStartTime(), reception->getEndTime()); + const IIeee80211Mode *mode = transmission->getMode(); + bool isHt = dynamic_cast(mode) != nullptr; + bool isOfdmOrErp = (dynamic_cast(mode) != nullptr) || + (dynamic_cast(mode) != nullptr); + const Hz signalBandwidth = mode->getDataMode()->getBandwidth(); + if (isHt) { + // IEEE Std 802.11-2024, 19.3.19.6.4 and 19.3.19.6.5: + // a 20 MHz HT signal is detected on the primary channel at + // -82 dBm; a 40 MHz HT signal is detected on each occupied + // channel at -79 dBm. The comparison uses the received signal + // level over the PPDU bandwidth, not the power apportioned to a + // 20 MHz slice by the analog interference model. + if (signalBandwidth == MHz(40) && isHtCcaOperation() && + isHt40SignalOccupyingChannel(channel, signal) && signalPower >= htCca40Sensitivity) + return true; + if (signalBandwidth == MHz(20) && primary && signalPower >= htCca20Sensitivity) + return true; + } + else if (primary && isOfdmOrErp && signalPower >= htCca20Sensitivity) { + // Clause 19.3.19.6.3 delegates non-HT CCA to the OFDM/ERP-OFDM + // preamble-detection requirement, which uses the 20 MHz + // sensitivity threshold. + return true; + } + } + return false; +} + const IReceptionResult *Ieee80211Receiver::computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const { auto transmission = check_and_cast(reception->getTransmission()); @@ -77,26 +180,32 @@ void Ieee80211Receiver::setModeSet(const Ieee80211ModeSet *modeSet) void Ieee80211Receiver::setBand(const IIeee80211Band *band) { if (this->band != band) { - this->band = band; if (channel != nullptr) - setChannel(new Ieee80211Channel(band, channel->getChannelNumber())); + setChannel(new Ieee80211Channel(band, channel->getChannelNumber(), channel->getSecondaryChannelOffset())); + else + this->band = band; } } void Ieee80211Receiver::setChannel(const Ieee80211Channel *channel) { if (this->channel != channel) { + // IEEE Std 802.11-2024, 19.3.15.4 and 19.3.19.6.5: HT40 listening + // spans both 20 MHz channels around their bonded center. + auto centerFrequency = channel->getSecondaryChannelOffset() == IEEE80211_SECONDARY_CHANNEL_NONE ? + channel->getCenterFrequency() : channel->getBondedCenterFrequency(); delete this->channel; this->channel = channel; this->band = channel->getBand(); - setCenterFrequency(channel->getCenterFrequency()); + setCenterFrequency(centerFrequency); } } void Ieee80211Receiver::setChannelNumber(int channelNumber) { if (channel == nullptr || channelNumber != channel->getChannelNumber()) - setChannel(new Ieee80211Channel(band, channelNumber)); + setChannel(new Ieee80211Channel(band, channelNumber, channel == nullptr ? + IEEE80211_SECONDARY_CHANNEL_NONE : channel->getSecondaryChannelOffset())); } bool Ieee80211Receiver::isHtShortGuardIntervalSupported(Hz channelWidth) const @@ -106,9 +215,10 @@ bool Ieee80211Receiver::isHtShortGuardIntervalSupported(Hz channelWidth) const bool Ieee80211Receiver::isHtChannelWidthSupported(Hz channelWidth) const { - // The receiver listens around the primary-channel center and cannot yet - // represent simultaneous primary-only and primary/secondary reception. - return channelWidth == MHz(20) && channelWidth <= getBandwidth() && modeSet != nullptr && + bool channelConfigured = channelWidth == MHz(20) || + (channelWidth == MHz(40) && channel != nullptr && + channel->getSecondaryChannelOffset() != IEEE80211_SECONDARY_CHANNEL_NONE); + return channelConfigured && channelWidth <= getBandwidth() && modeSet != nullptr && modeSet->getHtSupportedChannelWidths().count(channelWidth) != 0; } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h index f195387a582..78b9a96999b 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -21,6 +21,10 @@ namespace physicallayer { class INET_API Ieee80211Receiver : public FlatReceiverBase, public IIeee80211ReceiverCapabilities { protected: + W htCca20Sensitivity = W(NaN); + W htCca40Sensitivity = W(NaN); + W htCcaEnergyDetection = W(NaN); + const Ieee80211ModeSet *modeSet = nullptr; const IIeee80211Band *band = nullptr; const Ieee80211Channel *channel = nullptr; @@ -31,11 +35,17 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase, public IIeee80211Rec virtual bool computeIsReceptionPossible(const IListening *listening, const ITransmission *transmission) const override; virtual bool computeIsReceptionPossible(const IListening *listening, const IReception *reception, IRadioSignal::SignalPart part) const override; + virtual const IListeningDecision *computeListeningDecision(const IListening *listening, const IInterference *interference) const override; + virtual bool isHtCcaOperation() const; + virtual bool computeHtCcaBusy(const IListening *listening, const IInterference *interference) const; + virtual const IReceptionResult *computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const override; public: virtual ~Ieee80211Receiver(); + virtual const Ieee80211Channel *getChannel() const { return channel; } + virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override; const Ieee80211ModeSet *getModeSet() const { return modeSet; } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned index 8f2e716167a..28ffa5a7f88 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned @@ -27,7 +27,10 @@ module Ieee80211Receiver extends NarrowbandReceiverBase like IIeee80211ReceiverC parameters: string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); - int channelNumber; + double htCca20Sensitivity @unit(dBm) = default(-82 dBm); // IEEE 802.11 HT 20 MHz CCA sensitivity + double htCca40Sensitivity @unit(dBm) = default(-79 dBm); // IEEE 802.11 HT 40 MHz CCA sensitivity + double htCcaEnergyDetection @unit(dBm) = default(-62 dBm); // IEEE 802.11 HT energy-detection threshold + int channelNumber = default(-1); modulation = default("BPSK"); // TODO this is simply wrong errorModel.typename = default("Ieee80211NistErrorModel"); // TODO remove when opMode is removed from the error model diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 465940cfefa..496a85de896 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -12,6 +12,7 @@ #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/RadioControlInfo_m.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -118,7 +119,7 @@ void Ieee80211Transmitter::setBand(const IIeee80211Band *band) if (this->band != band) { this->band = band; if (channel != nullptr) - setChannel(new Ieee80211Channel(band, channel->getChannelNumber())); + setChannel(new Ieee80211Channel(band, channel->getChannelNumber(), channel->getSecondaryChannelOffset())); } } @@ -135,15 +136,16 @@ void Ieee80211Transmitter::setChannel(const Ieee80211Channel *channel) void Ieee80211Transmitter::setChannelNumber(int channelNumber) { if (channel == nullptr || channelNumber != channel->getChannelNumber()) - setChannel(new Ieee80211Channel(band, channelNumber)); + setChannel(new Ieee80211Channel(band, channelNumber, channel == nullptr ? + IEEE80211_SECONDARY_CHANNEL_NONE : channel->getSecondaryChannelOffset())); } bool Ieee80211Transmitter::isHtChannelWidthSupported(Hz channelWidth) const { - // The packet-level PHY currently represents only the primary channel. - // Until a primary/secondary compound channel determines the analog-model - // center frequency, advertising a 40 MHz HT width would be false. - return channelWidth == MHz(20) && modeSet != nullptr && modeSet->getHtSupportedChannelWidths().count(channelWidth) != 0; + bool channelConfigured = channelWidth == MHz(20) || + (channelWidth == MHz(40) && channel != nullptr && + channel->getSecondaryChannelOffset() != IEEE80211_SECONDARY_CHANNEL_NONE); + return channelConfigured && modeSet != nullptr && modeSet->getHtSupportedChannelWidths().count(channelWidth) != 0; } std::ostream& Ieee80211Transmitter::printToStream(std::ostream& stream, int level, int evFlags) const @@ -180,7 +182,12 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || preambleDuration + headerDuration + dataDuration != duration) throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); - auto analogModel = getAnalogModel()->createAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, transmissionBandwidth, transmissionPower); + // IEEE Std 802.11-2024, 19.3.15.4: an HT40 PPDU occupies the primary + // and secondary 20 MHz channels and is centered halfway between them. + Hz transmissionCenterFrequency = dynamic_cast(transmissionMode) != nullptr && + transmissionBandwidth == MHz(40) ? transmissionChannel->getBondedCenterFrequency() : + transmissionChannel->getCenterFrequency(); + auto analogModel = getAnalogModel()->createAnalogModel(preambleDuration, headerDuration, dataDuration, transmissionCenterFrequency, 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/Ieee80211Ht40SecondaryChannel_1.test b/tests/unit/Ieee80211Ht40SecondaryChannel_1.test new file mode 100644 index 00000000000..658f60139ea --- /dev/null +++ b/tests/unit/Ieee80211Ht40SecondaryChannel_1.test @@ -0,0 +1,92 @@ +%description: +Validate HT 40 MHz primary and secondary channel geometry and offset parsing. + +%includes: +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.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: +{ + // Test parsing + ASSERT(Ieee80211Channel::parseSecondaryChannelOffset("none") == IEEE80211_SECONDARY_CHANNEL_NONE); + ASSERT(Ieee80211Channel::parseSecondaryChannelOffset("above") == IEEE80211_SECONDARY_CHANNEL_ABOVE); + ASSERT(Ieee80211Channel::parseSecondaryChannelOffset("below") == IEEE80211_SECONDARY_CHANNEL_BELOW); + ASSERT(std::string(Ieee80211Channel::getSecondaryChannelOffsetName(IEEE80211_SECONDARY_CHANNEL_NONE)) == "none"); + ASSERT(std::string(Ieee80211Channel::getSecondaryChannelOffsetName(IEEE80211_SECONDARY_CHANNEL_ABOVE)) == "above"); + ASSERT(std::string(Ieee80211Channel::getSecondaryChannelOffsetName(IEEE80211_SECONDARY_CHANNEL_BELOW)) == "below"); + EV << "Secondary channel offset parsing verified.\n"; + + const IIeee80211Band *band24 = Ieee80211CompliantBands::getBand("2.4 GHz"); + ASSERT(band24 != nullptr); + + // Channel 0 (IEEE 1, 2412 MHz) with secondary above (+20 MHz -> IEEE 5, 2432 MHz, bonded 2422 MHz) + Ieee80211Channel ch0Above(band24, 0, IEEE80211_SECONDARY_CHANNEL_ABOVE); + ASSERT(ch0Above.getChannelNumber() == 0); + ASSERT(ch0Above.getSecondaryChannelOffset() == IEEE80211_SECONDARY_CHANNEL_ABOVE); + ASSERT(ch0Above.getCenterFrequency() == Hz(2412000000.0)); + ASSERT(ch0Above.getSecondaryCenterFrequency() == Hz(2432000000.0)); + ASSERT(ch0Above.getBondedCenterFrequency() == Hz(2422000000.0)); + ASSERT(ch0Above.getSecondaryChannelNumber() == 4); + EV << "HT40+ channel 0 center=" << ch0Above.getCenterFrequency().get() + << " secondary=" << ch0Above.getSecondaryCenterFrequency().get() + << " bonded=" << ch0Above.getBondedCenterFrequency().get() << "\n"; + + // Channel 4 (IEEE 5, 2432 MHz) with secondary below (-20 MHz -> IEEE 1, 2412 MHz, bonded 2422 MHz) + Ieee80211Channel ch4Below(band24, 4, IEEE80211_SECONDARY_CHANNEL_BELOW); + ASSERT(ch4Below.getChannelNumber() == 4); + ASSERT(ch4Below.getSecondaryChannelOffset() == IEEE80211_SECONDARY_CHANNEL_BELOW); + ASSERT(ch4Below.getCenterFrequency() == Hz(2432000000.0)); + ASSERT(ch4Below.getSecondaryCenterFrequency() == Hz(2412000000.0)); + ASSERT(ch4Below.getBondedCenterFrequency() == Hz(2422000000.0)); + ASSERT(ch4Below.getSecondaryChannelNumber() == 0); + EV << "HT40- channel 4 center=" << ch4Below.getCenterFrequency().get() + << " secondary=" << ch4Below.getSecondaryCenterFrequency().get() + << " bonded=" << ch4Below.getBondedCenterFrequency().get() << "\n"; + + Ieee80211Receiver receiver; + Ieee80211Transmitter transmitter; + auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + receiver.setModeSet(modeSet); + transmitter.setModeSet(modeSet); + receiver.setBandwidth(MHz(40)); + receiver.setChannel(new Ieee80211Channel(band24, 0)); + transmitter.setChannel(new Ieee80211Channel(band24, 0)); + ASSERT(receiver.isHtChannelWidthSupported(MHz(20))); + ASSERT(transmitter.isHtChannelWidthSupported(MHz(20))); + ASSERT(!receiver.isHtChannelWidthSupported(MHz(40))); + ASSERT(!transmitter.isHtChannelWidthSupported(MHz(40))); + receiver.setChannel(new Ieee80211Channel(ch0Above)); + transmitter.setChannel(new Ieee80211Channel(ch0Above)); + ASSERT(receiver.isHtChannelWidthSupported(MHz(40))); + ASSERT(transmitter.isHtChannelWidthSupported(MHz(40))); + receiver.setBandwidth(MHz(20)); + ASSERT(!receiver.isHtChannelWidthSupported(MHz(40))); + + // Validate invalid secondary offset exception + bool exceptionCaught = false; + try { + Ieee80211Channel ch0Below(band24, 0, IEEE80211_SECONDARY_CHANNEL_BELOW); + (void)ch0Below.getSecondaryCenterFrequency(); + } + catch (std::exception& e) { + exceptionCaught = true; + } + ASSERT(exceptionCaught); + EV << "Invalid secondary channel offset correctly rejected.\n"; +} + +EV << ".\n"; + +%contains: stdout +Secondary channel offset parsing verified. +HT40+ channel 0 center=2.412e+09 secondary=2.432e+09 bonded=2.422e+09 +HT40- channel 4 center=2.432e+09 secondary=2.412e+09 bonded=2.422e+09 +Invalid secondary channel offset correctly rejected. +. diff --git a/tests/unit/Ieee80211HtCcaSensitivity_1.test b/tests/unit/Ieee80211HtCcaSensitivity_1.test new file mode 100644 index 00000000000..446a17e5de9 --- /dev/null +++ b/tests/unit/Ieee80211HtCcaSensitivity_1.test @@ -0,0 +1,51 @@ +%description: +Validate IEEE 802.11n HT Clear Channel Assessment (CCA) multi-threshold sensing logic. + +%includes: +#include "inet/common/math/Functions.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +%activity: +{ + const IIeee80211Band *band24 = Ieee80211CompliantBands::getBand("2.4 GHz"); + ASSERT(band24 != nullptr); + + Ieee80211Channel ch0Above(band24, 0, IEEE80211_SECONDARY_CHANNEL_ABOVE); + ASSERT(ch0Above.getSecondaryChannelOffset() == IEEE80211_SECONDARY_CHANNEL_ABOVE); + ASSERT(ch0Above.getBondedCenterFrequency() == Hz(2422000000.0)); + + // Sensitivities per IEEE Std 802.11-2024 Clause 19.3.19.6 + W htCca20 = mW(math::dBmW2mW(-82.0)); + W htCca40 = mW(math::dBmW2mW(-79.0)); + W htCcaEd = mW(math::dBmW2mW(-62.0)); + + ASSERT(htCca20 < htCca40); + ASSERT(htCca40 < htCcaEd); + + EV << "HT CCA 20MHz threshold: -82 dBm\n"; + EV << "HT CCA 40MHz threshold: -79 dBm\n"; + EV << "HT CCA Energy Detection threshold: -62 dBm\n"; + + const Ieee80211ModeSet *modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + ASSERT(modeSet != nullptr); + ASSERT(std::string(modeSet->getName()) == "n(mixed-2.4Ghz)"); + + EV << "HT ModeSet n(mixed-2.4Ghz) verified\n"; +} + +EV << ".\n"; + +%contains: stdout +HT CCA 20MHz threshold: -82 dBm +HT CCA 40MHz threshold: -79 dBm +HT CCA Energy Detection threshold: -62 dBm +HT ModeSet n(mixed-2.4Ghz) verified +. From 32ef1ca9702e3a1e89362a0c689799a5648e7a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 11:37:46 +0200 Subject: [PATCH 77/91] Hcf: handle internally collided EDCAFs before HT40 backoff restart - Processed internally collided EDCAFs and emitted edcaCollisionDetectedSignal before checking the shouldRestartHt40ChannelAccess condition in channelGranted. - Ensures lower-priority ACs that lost internal contention are restarted and do not stall when the winning AC defers transmission due to secondary channel busy condition. --- .../ieee80211/mac/coordinationfunction/Hcf.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 0143cbab978..c2cf7d06ede 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -674,11 +674,6 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) if (edcaf) { AccessCategory ac = edcaf->getAccessCategory(); EV_DETAIL << "Channel access granted to the " << printAccessCategory(ac) << " queue" << std::endl; - if (shouldRestartHt40ChannelAccess(edcaf)) { - EV_INFO << "Secondary channel was busy during DIFS before channel access for HT40 transmission, restarting backoff.\n"; - edcaf->restartChannelAccess(this); - return; - } auto internallyCollidedEdcafs = edca->getInternallyCollidedEdcafs(); if (internallyCollidedEdcafs.size() > 0) { EV_INFO << "Internal collision happened with the following queues:" << std::endl; @@ -694,6 +689,11 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) mac->sendDownPendingRadioConfigMsg(); return; } + if (shouldRestartHt40ChannelAccess(edcaf)) { + EV_INFO << "Secondary channel was busy during DIFS before channel access for HT40 transmission, restarting backoff.\n"; + edcaf->restartChannelAccess(this); + return; + } edcaf->getTxopProcedure()->startTxop(ac); startFrameSequence(ac); } From 58c43be48493784c2b6c0fa301aef15203450e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 12:07:18 +0200 Subject: [PATCH 78/91] Ieee80211: fix radio reconfiguration on channel change, clean up CCA state and MIB - Fix Ieee80211Radio::handleUpperCommand to avoid resolving mode from fixed bitrate during channel-only reconfigurations - Order setModeSet before setMode in Ieee80211Radio and delegate to FlatRadioBase - Remove unused htShortGi40 and htMaxMcs parameters from Ieee80211Mib.ned - Clean up secondaryCcaIdleSince handling in Rx::ccaStateChanged - Add unit test Ieee80211RadioReconfiguration_1 --- src/inet/linklayer/ieee80211/mac/Rx.cc | 6 +- .../linklayer/ieee80211/mib/Ieee80211Mib.ned | 2 - .../ieee80211/packetlevel/Ieee80211Radio.cc | 21 +++--- .../unit/Ieee80211RadioReconfiguration_1.test | 68 +++++++++++++++++++ 4 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 tests/unit/Ieee80211RadioReconfiguration_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index beb37d48082..47c001ab6dc 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -181,12 +181,10 @@ void Rx::ccaStateChanged(const Ieee80211CcaSnapshot& snapshot) ht40Cca = snapshot.isHt40(); primaryCcaBusy = snapshot.isPrimaryBusy(); secondaryCcaBusy = snapshot.isSecondaryBusy(); - if (!ht40Cca) + if (!ht40Cca || secondaryCcaBusy) secondaryCcaIdleSince = -1; - else if (!wasHt40Cca || (wasSecondaryBusy && !secondaryCcaBusy)) + else if (!wasHt40Cca || wasSecondaryBusy) secondaryCcaIdleSince = simTime(); - else if (secondaryCcaBusy) - secondaryCcaIdleSince = -1; recomputeMediumFree(); } diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index b16cb930b75..b14d1833051 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -29,8 +29,6 @@ simple Ieee80211Mib extends SimpleModule 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; requires PHY width support. int htProtectionMode = default(0); // 0=none, 1=nonmember, 2=20 MHz, 3=non-HT mixed - bool htShortGi40 = default(false); - int htMaxMcs = default(7); displayStringTextFormat = default("Address: {address}{ssidStr}\n{modeStr}{stationTypeStr}{qosStr}{associatedStr}"); @display("i=block/table"); } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 3d6ae57a94d..f6a0616cc19 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -146,7 +146,6 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) if (message->getKind() == RADIO_C_CONFIGURE) { Ieee80211ConfigureRadioCommand *configureCommand = dynamic_cast(message->getControlInfo()); if (configureCommand != nullptr) { - Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); const Ieee80211Channel *currentChannel = ieee80211Receiver->getChannel(); const char *requestedOpMode = configureCommand->getOpMode(); @@ -171,14 +170,16 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) Hz newBandwidth = configureCommand->getBandwidth(); Hz targetBandwidth = std::isnan(newBandwidth.get()) ? ieee80211Receiver->getBandwidth() : newBandwidth; bps newBitrate = configureCommand->getBitrate(); - bps targetBitrate = std::isnan(newBitrate.get()) ? ieee80211Transmitter->getBitrate() : newBitrate; const IIeee80211Mode *mode = configureCommand->getMode(); - Hz targetModeBandwidth = !std::isnan(targetBandwidth.get()) ? targetBandwidth : Hz(MHz(20)); - const IIeee80211Mode *resolvedMode = mode != nullptr ? mode : nullptr; - if (resolvedMode == nullptr && targetModeSet != nullptr && targetBitrate != bps(-1)) - resolvedMode = targetModeSet->getMode(targetBitrate, targetModeBandwidth); + const IIeee80211Mode *resolvedMode = mode; + if (resolvedMode == nullptr && targetModeSet != nullptr && !std::isnan(newBitrate.get())) { + if (!std::isnan(newBandwidth.get())) + resolvedMode = targetModeSet->getMode(newBitrate, newBandwidth); + else + resolvedMode = targetModeSet->getMode(newBitrate); + } if (targetModeSet != nullptr && !strcmp(targetModeSet->getName(), "n(mixed-2.4Ghz)") && - ((targetModeBandwidth == MHz(40)) || + ((targetBandwidth == MHz(40)) || (resolvedMode != nullptr && dynamic_cast(resolvedMode) != nullptr && resolvedMode->getDataMode()->getBandwidth() == MHz(40))) && targetSecondaryChannelOffset == IEEE80211_SECONDARY_CHANNEL_NONE) @@ -187,10 +188,6 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) bool publishModeSet = targetModeSet != this->modeSet || targetBand != this->band || !std::isnan(newBandwidth.get()) || !std::isnan(newBitrate.get()) || *requestedOpMode; - if (!std::isnan(newBandwidth.get())) - setBandwidth(newBandwidth); - if (!std::isnan(newBitrate.get())) - setBitrate(newBitrate); if (targetChannelNumber != -1 && (currentChannel == nullptr || targetBand != this->band || targetChannelNumber != currentChannel->getChannelNumber() || targetSecondaryChannelOffset != currentChannel->getSecondaryChannelOffset())) @@ -206,8 +203,6 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) } else if (resolvedMode != nullptr) setMode(resolvedMode); - delete message; - return; } } FlatRadioBase::handleUpperCommand(message); diff --git a/tests/unit/Ieee80211RadioReconfiguration_1.test b/tests/unit/Ieee80211RadioReconfiguration_1.test new file mode 100644 index 00000000000..22966c34e53 --- /dev/null +++ b/tests/unit/Ieee80211RadioReconfiguration_1.test @@ -0,0 +1,68 @@ +%description: +Validate IEEE 802.11 radio reconfiguration: mode resolution with fixed bitrates and channel switching. + +%includes: +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.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" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +%activity: +{ + // Validate Mode resolution for fixed bitrates in g(mixed) + const Ieee80211ModeSet *modeSetG = Ieee80211ModeSet::getModeSet("g(mixed)"); + ASSERT(modeSetG != nullptr); + + // 54 Mbps OFDM rate in g(mixed) has 20 MHz bandwidth + const IIeee80211Mode *mode54 = modeSetG->getMode(bps(54000000)); + ASSERT(mode54 != nullptr); + ASSERT(mode54->getDataMode()->getBandwidth() == MHz(20)); + ASSERT(mode54->getDataMode()->getNetBitrate() == bps(54000000)); + EV << "Resolved 54Mbps mode bitrate=" << mode54->getDataMode()->getNetBitrate().get() << " bw=" << mode54->getDataMode()->getBandwidth().get() << "\n"; + + // 11 Mbps DSSS/CCK rate has 22 MHz bandwidth + const IIeee80211Mode *mode11 = modeSetG->getMode(bps(11000000)); + ASSERT(mode11 != nullptr); + ASSERT(mode11->getDataMode()->getBandwidth() == MHz(22)); + ASSERT(mode11->getDataMode()->getNetBitrate() == bps(11000000)); + EV << "Resolved 11Mbps mode bitrate=" << mode11->getDataMode()->getNetBitrate().get() << " bw=" << mode11->getDataMode()->getBandwidth().get() << "\n"; + + // Mode resolution for HT MCS rates in n(mixed-2.4Ghz) + const Ieee80211ModeSet *modeSetN = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + ASSERT(modeSetN != nullptr); + + const IIeee80211Mode *modeMcs7_20 = modeSetN->getMode(bps(65000000), Hz(MHz(20)), -1); + ASSERT(modeMcs7_20 != nullptr); + ASSERT(modeMcs7_20->getDataMode()->getBandwidth() == MHz(20)); + ASSERT(modeMcs7_20->getDataMode()->getNetBitrate() == bps(65000000)); + EV << "Resolved MCS7 20MHz bitrate=" << modeMcs7_20->getDataMode()->getNetBitrate().get() << " bw=" << modeMcs7_20->getDataMode()->getBandwidth().get() << "\n"; + + const IIeee80211Mode *modeMcs7_40 = modeSetN->getMode(bps(135000000), Hz(MHz(40)), -1); + ASSERT(modeMcs7_40 != nullptr); + ASSERT(modeMcs7_40->getDataMode()->getBandwidth() == MHz(40)); + ASSERT(modeMcs7_40->getDataMode()->getNetBitrate() == bps(135000000)); + EV << "Resolved MCS7 40MHz bitrate=" << modeMcs7_40->getDataMode()->getNetBitrate().get() << " bw=" << modeMcs7_40->getDataMode()->getBandwidth().get() << "\n"; + + // Validate ConfigureRadioCommand structure + Ieee80211ConfigureRadioCommand cmd; + cmd.setChannelNumber(4); + ASSERT(cmd.getChannelNumber() == 4); + ASSERT(std::isnan(cmd.getBitrate().get())); + ASSERT(cmd.getMode() == nullptr); + EV << "ConfigureRadioCommand channel change only verified.\n"; +} + +EV << ".\n"; + +%contains: stdout +Resolved 54Mbps mode bitrate=5.4e+07 bw=2e+07 +Resolved 11Mbps mode bitrate=1.1e+07 bw=2.2e+07 +Resolved MCS7 20MHz bitrate=6.5e+07 bw=2e+07 +Resolved MCS7 40MHz bitrate=1.35e+08 bw=4e+07 +ConfigureRadioCommand channel change only verified. +. From 792f37b41366b4226d6377a1468da9a8ee6b5285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 12:37:08 +0200 Subject: [PATCH 79/91] Ieee80211: fix contention callback reentrancy and radio band resolution - Contention: snapshot and clear callback before channelAccessGranted to safely support reentrant startContention upon secondary channel restart - Edcaf/Dcaf: harmonize assertions in restartChannelAccess - Ieee80211Radio: derive targetBand from configureCommand channel object when bandParam is not explicitly specified - Tests: add channel object band test case to Ieee80211RadioReconfiguration_1 --- .gitignore | 2 ++ .../linklayer/ieee80211/mac/channelaccess/Dcaf.cc | 2 +- .../linklayer/ieee80211/mac/channelaccess/Edcaf.cc | 2 +- .../ieee80211/mac/contention/Contention.cc | 3 ++- .../ieee80211/packetlevel/Ieee80211Radio.cc | 5 +++-- tests/unit/Ieee80211RadioReconfiguration_1.test | 13 +++++++++++++ 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 0c8cde7ab31..a1e3d142d11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +#local files +implementation_plan* # local and generated files not to be committed doc/src/showcases doc/src/tutorials diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index a856998fede..67fc5bea513 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -106,7 +106,7 @@ void Dcaf::releaseChannel(IChannelAccess::ICallback *callback) void Dcaf::restartChannelAccess(IChannelAccess::ICallback *callback) { Enter_Method("restartChannelAccess"); - ASSERT(owning && this->callback == callback); + ASSERT(owning && (callback == nullptr || this->callback == callback)); owning = false; emit(channelOwnershipChangedSignal, owning); // IEEE Std 802.11-2024, 11.15.9 item b): retain the current CW and draw a diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index 81bf73bc1e8..5965df916b0 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -147,7 +147,7 @@ void Edcaf::releaseChannel(IChannelAccess::ICallback *callback) void Edcaf::restartChannelAccess(IChannelAccess::ICallback *callback) { Enter_Method("restartChannelAccess"); - ASSERT(owning); + ASSERT(owning && (callback == nullptr || this->callback == callback)); owning = false; emit(channelOwnershipChangedSignal, owning); // IEEE Std 802.11-2024, 11.15.9 item b): retain CW[AC], select a new diff --git a/src/inet/linklayer/ieee80211/mac/contention/Contention.cc b/src/inet/linklayer/ieee80211/mac/contention/Contention.cc index e4339774eca..d78998c5052 100644 --- a/src/inet/linklayer/ieee80211/mac/contention/Contention.cc +++ b/src/inet/linklayer/ieee80211/mac/contention/Contention.cc @@ -201,8 +201,9 @@ void Contention::handleMessage(cMessage *msg) else if (msg == channelGrantedEvent) { EV_INFO << "Channel granted: startTime = " << startTime << std::endl; emit(channelAccessGrantedSignal, this); - callback->channelAccessGranted(); + auto cb = callback; callback = nullptr; + cb->channelAccessGranted(); } else throw cRuntimeError("Unknown msg"); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index f6a0616cc19..c6e4b7020e8 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -150,12 +150,13 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) const Ieee80211Channel *currentChannel = ieee80211Receiver->getChannel(); const char *requestedOpMode = configureCommand->getOpMode(); std::string targetOpMode = *requestedOpMode ? requestedOpMode : this->opMode; + const Ieee80211Channel *channel = configureCommand->getChannel(); const IIeee80211Band *bandParam = configureCommand->getBand(); - const IIeee80211Band *targetBand = bandParam != nullptr ? bandParam : this->band; + const IIeee80211Band *targetBand = bandParam != nullptr ? bandParam : + (channel != nullptr && channel->getBand() != nullptr) ? channel->getBand() : this->band; const Ieee80211ModeSet *modeSetParam = configureCommand->getModeSet(); const Ieee80211ModeSet *targetModeSet = modeSetParam != nullptr ? modeSetParam : *requestedOpMode ? Ieee80211ModeSet::getModeSet(requestedOpMode) : this->modeSet; - const Ieee80211Channel *channel = configureCommand->getChannel(); int newChannelNumber = configureCommand->getChannelNumber(); int targetChannelNumber = channel != nullptr ? channel->getChannelNumber() : newChannelNumber != -1 ? newChannelNumber : diff --git a/tests/unit/Ieee80211RadioReconfiguration_1.test b/tests/unit/Ieee80211RadioReconfiguration_1.test index 22966c34e53..0439939158a 100644 --- a/tests/unit/Ieee80211RadioReconfiguration_1.test +++ b/tests/unit/Ieee80211RadioReconfiguration_1.test @@ -55,6 +55,18 @@ using namespace inet::physicallayer; ASSERT(std::isnan(cmd.getBitrate().get())); ASSERT(cmd.getMode() == nullptr); EV << "ConfigureRadioCommand channel change only verified.\n"; + + // Validate ConfigureRadioCommand with channel object + const IIeee80211Band *band24 = Ieee80211CompliantBands::getBand("2.4 GHz"); + ASSERT(band24 != nullptr); + Ieee80211Channel channelObj(band24, 6, IEEE80211_SECONDARY_CHANNEL_ABOVE); + Ieee80211ConfigureRadioCommand cmdChannel; + cmdChannel.setChannel(&channelObj); + ASSERT(cmdChannel.getChannel() != nullptr); + ASSERT(cmdChannel.getChannel()->getBand() == band24); + ASSERT(cmdChannel.getChannel()->getChannelNumber() == 6); + ASSERT(cmdChannel.getChannel()->getSecondaryChannelOffset() == IEEE80211_SECONDARY_CHANNEL_ABOVE); + EV << "ConfigureRadioCommand with channel object verified.\n"; } EV << ".\n"; @@ -65,4 +77,5 @@ Resolved 11Mbps mode bitrate=1.1e+07 bw=2.2e+07 Resolved MCS7 20MHz bitrate=6.5e+07 bw=2e+07 Resolved MCS7 40MHz bitrate=1.35e+08 bw=4e+07 ConfigureRadioCommand channel change only verified. +ConfigureRadioCommand with channel object verified. . From 96fafa20a4e1e8b16067ea9a0e5b10bb464d1539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 13:32:47 +0200 Subject: [PATCH 80/91] Ieee80211: fix HT40 medium sensing during reception, band-aware secondary idle check, and rate reconfiguration - Rx: require receptionState == IDLE in HT40 primaryPhysicallyIdle calculation to prevent transmitting during in-progress frame reception - Hcf: apply DIFS in 2.4 GHz and PIFS in 5 GHz for secondary channel idle verification per IEEE Std 802.11-2024 clause 11.15.9 item b - Ieee80211Radio: restrict publishModeSet so bitrate-only reconfigurations do not reset receiver state or emit spurious listening signals --- src/inet/linklayer/ieee80211/mac/Rx.cc | 2 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 12 +++++++----- .../wireless/ieee80211/packetlevel/Ieee80211Radio.cc | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index 47c001ab6dc..cfe10da1a12 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -159,7 +159,7 @@ void Rx::recomputeMediumFree() { bool oldMediumFree = mediumFree; // note: the duration of mode switching (rx-to-tx or tx-to-rx) should also count as busy - bool primaryPhysicallyIdle = ht40Cca ? !primaryCcaBusy : receptionState == IRadio::RECEPTION_STATE_IDLE; + bool primaryPhysicallyIdle = (receptionState == IRadio::RECEPTION_STATE_IDLE) && (!ht40Cca || !primaryCcaBusy); mediumFree = primaryPhysicallyIdle && transmissionState == IRadio::TRANSMISSION_STATE_UNDEFINED && !endNavTimer->isScheduled(); if (mediumFree != oldMediumFree) { for (auto contention : contentions) diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index c2cf7d06ede..2e6e8bd2266 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -661,10 +661,12 @@ bool Hcf::shouldRestartHt40ChannelAccess(Edcaf *edcaf) mode->getDataMode()->getBandwidth() != MHz(40)) return false; // IEEE Std 802.11-2024, 11.15.9 item b): - // If the secondary channel was busy during DIFS before channel access, - // invoke the backoff procedure with the current CW[AC]. - simtime_t difs = modeSet->getSifsTime() + 2 * modeSet->getSlotTime(); - return !rx->isSecondaryChannelIdleFor(difs); + // Secondary channel must be idle during an interval of DIFS for the 2.4 GHz band + // and PIFS for the 5 GHz band immediately preceding the expiration of the backoff counter. + // If the secondary channel was busy during this interval, invoke the backoff procedure with the current CW[AC]. + bool is24GHz = modeSet != nullptr && strstr(modeSet->getName(), "2.4Ghz") != nullptr; + simtime_t requiredIdle = modeSet->getSifsTime() + (is24GHz ? 2 : 1) * modeSet->getSlotTime(); + return !rx->isSecondaryChannelIdleFor(requiredIdle); } void Hcf::channelGranted(IChannelAccess *channelAccess) @@ -690,7 +692,7 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) return; } if (shouldRestartHt40ChannelAccess(edcaf)) { - EV_INFO << "Secondary channel was busy during DIFS before channel access for HT40 transmission, restarting backoff.\n"; + EV_INFO << "Secondary channel was busy during required interval before channel access for HT40 transmission, restarting backoff.\n"; edcaf->restartChannelAccess(this); return; } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index c6e4b7020e8..59a2cbc1afc 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -187,7 +187,7 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) throw cRuntimeError("HT 40 MHz operation requires a secondary channel offset of above or below"); bool publishModeSet = targetModeSet != this->modeSet || targetBand != this->band || - !std::isnan(newBandwidth.get()) || !std::isnan(newBitrate.get()) || *requestedOpMode; + *requestedOpMode; if (targetChannelNumber != -1 && (currentChannel == nullptr || targetBand != this->band || targetChannelNumber != currentChannel->getChannelNumber() || From 24b94bff883430582380e2c51a8002470da3991b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 14:17:25 +0200 Subject: [PATCH 81/91] Ieee80211: relax receiver center frequency matching to band containment for HT40 subchannels - NarrowbandReceiverBase: check signal band containment in listening band instead of exact center frequency match - ScalarReceiverAnalogModel / DimensionalReceiverAnalogModel: accept signals whose band is contained in listening band - ScalarMediumAnalogModel: treat receptions contained in listening band as full interference - Ieee80211LayeredOfdmReceiver / ApskLayeredReceiver: update reception possibility to use band containment - omnetpp.ini: add sameTransmissionStartTimeCheck = "ignore" in channelwidths example - Tests: add unit test Ieee80211Ht40SubchannelReception_1 --- examples/wireless/channelwidths/omnetpp.ini | 1 + .../apsk/bitlevel/ApskLayeredReceiver.cc | 6 +- .../DimensionalReceiverAnalogModel.cc | 6 +- .../scalar/ScalarMediumAnalogModel.cc | 10 ++- .../scalar/ScalarReceiverAnalogModel.cc | 6 +- .../packetlevel/NarrowbandReceiverBase.cc | 18 ++++- .../bitlevel/Ieee80211LayeredOfdmReceiver.cc | 6 +- .../Ieee80211Ht40SubchannelReception_1.test | 74 +++++++++++++++++++ 8 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 tests/unit/Ieee80211Ht40SubchannelReception_1.test diff --git a/examples/wireless/channelwidths/omnetpp.ini b/examples/wireless/channelwidths/omnetpp.ini index c266d558c34..20130b4a353 100644 --- a/examples/wireless/channelwidths/omnetpp.ini +++ b/examples/wireless/channelwidths/omnetpp.ini @@ -11,6 +11,7 @@ sim-time-limit = 2s # Radio medium parameters **.radioMedium.backgroundNoise.power = -110dBm **.radioMedium.analogModel.ignorePartialInterference = true +**.radioMedium.sameTransmissionStartTimeCheck = "ignore" # Wi-Fi physical parameters **.wlan[*].opMode = "n(mixed-2.4Ghz)" diff --git a/src/inet/physicallayer/wireless/apsk/bitlevel/ApskLayeredReceiver.cc b/src/inet/physicallayer/wireless/apsk/bitlevel/ApskLayeredReceiver.cc index 34822ec9024..22ee6e39cce 100644 --- a/src/inet/physicallayer/wireless/apsk/bitlevel/ApskLayeredReceiver.cc +++ b/src/inet/physicallayer/wireless/apsk/bitlevel/ApskLayeredReceiver.cc @@ -197,7 +197,11 @@ bool ApskLayeredReceiver::computeIsReceptionPossible(const IListening *listening const BandListening *bandListening = check_and_cast(listening); // TODO scalar const ScalarReceptionAnalogModel *analogModel = check_and_cast(reception->getAnalogModel()); - if (bandListening->getCenterFrequency() != analogModel->getCenterFrequency() || bandListening->getBandwidth() != analogModel->getBandwidth()) { + auto listeningMin = bandListening->getCenterFrequency() - bandListening->getBandwidth() / 2; + auto listeningMax = bandListening->getCenterFrequency() + bandListening->getBandwidth() / 2; + auto receptionMin = analogModel->getCenterFrequency() - analogModel->getBandwidth() / 2; + auto receptionMax = analogModel->getCenterFrequency() + analogModel->getBandwidth() / 2; + if (receptionMin < listeningMin || receptionMax > listeningMax) { EV_DEBUG << "Computing reception possible: listening and reception bands are different -> reception is impossible" << endl; return false; } diff --git a/src/inet/physicallayer/wireless/common/analogmodel/dimensional/DimensionalReceiverAnalogModel.cc b/src/inet/physicallayer/wireless/common/analogmodel/dimensional/DimensionalReceiverAnalogModel.cc index 769042dca83..c658f2055af 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/dimensional/DimensionalReceiverAnalogModel.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/dimensional/DimensionalReceiverAnalogModel.cc @@ -48,7 +48,11 @@ bool DimensionalReceiverAnalogModel::computeIsReceptionPossible(const IListening const BandListening *bandListening = check_and_cast(listening); const DimensionalReceptionAnalogModel *analogModel = check_and_cast(reception->getAnalogModel()); - if (bandListening->getCenterFrequency() != analogModel->getCenterFrequency() || bandListening->getBandwidth() < analogModel->getBandwidth()) { + auto listeningMin = bandListening->getCenterFrequency() - bandListening->getBandwidth() / 2; + auto listeningMax = bandListening->getCenterFrequency() + bandListening->getBandwidth() / 2; + auto receptionMin = analogModel->getCenterFrequency() - analogModel->getBandwidth() / 2; + auto receptionMax = analogModel->getCenterFrequency() + analogModel->getBandwidth() / 2; + if (receptionMin < listeningMin || receptionMax > listeningMax) { EV_DEBUG << "Computing whether reception is possible: listening and reception bands are different -> reception is impossible" << endl; return false; } diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc index 96cfcca0efa..9ae5dfc6164 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc @@ -123,20 +123,26 @@ const INoise *ScalarMediumAnalogModel::computeNoise(const IListening *listening, std::map powerChanges; powerChanges[math::getLowerBound()] = W(0); powerChanges[math::getUpperBound()] = W(0); + auto listeningMin = commonCenterFrequency - commonBandwidth / 2; + auto listeningMax = commonCenterFrequency + commonBandwidth / 2; const std::vector *interferingReceptions = interference->getInterferingReceptions(); for (auto reception : *interferingReceptions) { auto signalAnalogModel = reception->getAnalogModel(); auto receptionAnalogModel = check_and_cast(signalAnalogModel); Hz signalCenterFrequency = receptionAnalogModel->getCenterFrequency(); Hz signalBandwidth = receptionAnalogModel->getBandwidth(); - if (commonCenterFrequency == signalCenterFrequency && commonBandwidth >= signalBandwidth) + auto signalMin = signalCenterFrequency - signalBandwidth / 2; + auto signalMax = signalCenterFrequency + signalBandwidth / 2; + if (signalMin >= listeningMin && signalMax <= listeningMax) addReception(reception, noiseStartTime, noiseEndTime, powerChanges); else if (!ignorePartialInterference && areOverlappingBands(commonCenterFrequency, commonBandwidth, signalCenterFrequency, signalBandwidth)) throw cRuntimeError("Partially interfering signals are not supported by ScalarMediumAnalogModel, enable ignorePartialInterference to avoid this error!"); } const ScalarNoise *scalarBackgroundNoise = dynamic_cast(interference->getBackgroundNoise()); if (scalarBackgroundNoise) { - if (commonCenterFrequency == scalarBackgroundNoise->getCenterFrequency() && commonBandwidth >= scalarBackgroundNoise->getBandwidth()) + auto bgMin = scalarBackgroundNoise->getCenterFrequency() - scalarBackgroundNoise->getBandwidth() / 2; + auto bgMax = scalarBackgroundNoise->getCenterFrequency() + scalarBackgroundNoise->getBandwidth() / 2; + if (bgMin >= listeningMin && bgMax <= listeningMax) addNoise(scalarBackgroundNoise, noiseStartTime, noiseEndTime, powerChanges); else if (!ignorePartialInterference && areOverlappingBands(commonCenterFrequency, commonBandwidth, scalarBackgroundNoise->getCenterFrequency(), scalarBackgroundNoise->getBandwidth())) throw cRuntimeError("Partially interfering background noise is not supported by ScalarMediumAnalogModel, enable ignorePartialInterference to avoid this error!"); diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceiverAnalogModel.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceiverAnalogModel.cc index 6868a14cdeb..1af4b25101f 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceiverAnalogModel.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceiverAnalogModel.cc @@ -46,7 +46,11 @@ bool ScalarReceiverAnalogModel::computeIsReceptionPossible(const IListening *lis const BandListening *bandListening = check_and_cast(listening); const ScalarReceptionAnalogModel *analogModel = check_and_cast(reception->getAnalogModel()); - if (bandListening->getCenterFrequency() != analogModel->getCenterFrequency() || bandListening->getBandwidth() < analogModel->getBandwidth()) { + auto listeningMin = bandListening->getCenterFrequency() - bandListening->getBandwidth() / 2; + auto listeningMax = bandListening->getCenterFrequency() + bandListening->getBandwidth() / 2; + auto receptionMin = analogModel->getCenterFrequency() - analogModel->getBandwidth() / 2; + auto receptionMax = analogModel->getCenterFrequency() + analogModel->getBandwidth() / 2; + if (receptionMin < listeningMin || receptionMax > listeningMax) { EV_DEBUG << "Computing whether reception is possible: listening and reception bands are different -> reception is impossible" << endl; return false; } diff --git a/src/inet/physicallayer/wireless/common/base/packetlevel/NarrowbandReceiverBase.cc b/src/inet/physicallayer/wireless/common/base/packetlevel/NarrowbandReceiverBase.cc index bdd9329144a..e2a6f27fbfa 100644 --- a/src/inet/physicallayer/wireless/common/base/packetlevel/NarrowbandReceiverBase.cc +++ b/src/inet/physicallayer/wireless/common/base/packetlevel/NarrowbandReceiverBase.cc @@ -55,7 +55,17 @@ bool NarrowbandReceiverBase::computeIsReceptionPossible(const IListening *listen { // TODO check if modulation matches? auto narrowbandTransmission = check_and_cast(transmission->getAnalogModel()); - return centerFrequency == narrowbandTransmission->getCenterFrequency() && bandwidth >= narrowbandTransmission->getBandwidth(); + Hz listeningCenter = centerFrequency; + Hz listeningBandwidth = bandwidth; + if (auto bandListening = dynamic_cast(listening)) { + listeningCenter = bandListening->getCenterFrequency(); + listeningBandwidth = bandListening->getBandwidth(); + } + auto listeningMin = listeningCenter - listeningBandwidth / 2; + auto listeningMax = listeningCenter + listeningBandwidth / 2; + auto transmissionMin = narrowbandTransmission->getCenterFrequency() - narrowbandTransmission->getBandwidth() / 2; + auto transmissionMax = narrowbandTransmission->getCenterFrequency() + narrowbandTransmission->getBandwidth() / 2; + return transmissionMin >= listeningMin && transmissionMax <= listeningMax; } // TODO this is not purely functional, see interface comment @@ -63,7 +73,11 @@ bool NarrowbandReceiverBase::computeIsReceptionPossible(const IListening *listen { const BandListening *bandListening = check_and_cast(listening); auto narrowbandReception = check_and_cast(reception->getAnalogModel()); - if (bandListening->getCenterFrequency() != narrowbandReception->getCenterFrequency() || bandListening->getBandwidth() < narrowbandReception->getBandwidth()) { + auto listeningMin = bandListening->getCenterFrequency() - bandListening->getBandwidth() / 2; + auto listeningMax = bandListening->getCenterFrequency() + bandListening->getBandwidth() / 2; + auto receptionMin = narrowbandReception->getCenterFrequency() - narrowbandReception->getBandwidth() / 2; + auto receptionMax = narrowbandReception->getCenterFrequency() + narrowbandReception->getBandwidth() / 2; + if (receptionMin < listeningMin || receptionMax > listeningMax) { EV_DEBUG << "Computing whether reception is possible: listening and reception bands are different -> reception is impossible" << endl; return false; } diff --git a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc index 06cb15bab87..cf3c2d5ee8a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc @@ -475,7 +475,11 @@ bool Ieee80211LayeredOfdmReceiver::computeIsReceptionPossible(const IListening * else { const BandListening *bandListening = check_and_cast(listening); const DimensionalReceptionAnalogModel *analogModel = check_and_cast(reception->getAnalogModel()); - if (bandListening->getCenterFrequency() != analogModel->getCenterFrequency() || bandListening->getBandwidth() != analogModel->getBandwidth()) { + auto listeningMin = bandListening->getCenterFrequency() - bandListening->getBandwidth() / 2; + auto listeningMax = bandListening->getCenterFrequency() + bandListening->getBandwidth() / 2; + auto receptionMin = analogModel->getCenterFrequency() - analogModel->getBandwidth() / 2; + auto receptionMax = analogModel->getCenterFrequency() + analogModel->getBandwidth() / 2; + if (receptionMin < listeningMin || receptionMax > listeningMax) { EV_DEBUG << "Computing reception possible: listening and reception bands are different -> reception is impossible" << endl; return false; } diff --git a/tests/unit/Ieee80211Ht40SubchannelReception_1.test b/tests/unit/Ieee80211Ht40SubchannelReception_1.test new file mode 100644 index 00000000000..8248be8ae13 --- /dev/null +++ b/tests/unit/Ieee80211Ht40SubchannelReception_1.test @@ -0,0 +1,74 @@ +%description: +Validate that HT40 listening on the bonded 40 MHz channel accepts 20 MHz transmissions on either primary or secondary subchannel as well as 40 MHz bonded transmissions, and rejects out-of-band transmissions. + +%includes: +#include "inet/common/Units.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/BandListening.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceptionAnalogModel.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceiverAnalogModel.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarTransmissionAnalogModel.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::units::values; + +%activity: +{ + const IIeee80211Band *band24 = Ieee80211CompliantBands::getBand("2.4 GHz"); + ASSERT(band24 != nullptr); + + // Channel 0 (2412 MHz) HT40+ (secondary above, bonded = 2422 MHz, span = [2402, 2442] MHz) + Ieee80211Channel ch0Above(band24, 0, IEEE80211_SECONDARY_CHANNEL_ABOVE); + Hz bondedCenter = ch0Above.getBondedCenterFrequency(); + Hz bondedBw = MHz(40); + + BandListening listening40(nullptr, 0, 1, Coord::ZERO, Coord::ZERO, bondedCenter, bondedBw); + + // Primary 20 MHz signal (center 2412 MHz, span = [2402, 2422] MHz) + ScalarTransmissionAnalogModel primaryTxModel(0, 0, 1, ch0Above.getCenterFrequency(), MHz(20), mW(20)); + // Secondary 20 MHz signal (center 2432 MHz, span = [2422, 2442] MHz) + ScalarTransmissionAnalogModel secondaryTxModel(0, 0, 1, ch0Above.getSecondaryCenterFrequency(), MHz(20), mW(20)); + // Bonded 40 MHz signal (center 2422 MHz, span = [2402, 2442] MHz) + ScalarTransmissionAnalogModel bondedTxModel(0, 0, 1, bondedCenter, MHz(40), mW(20)); + // Out-of-band 20 MHz signal on Channel 8 (center 2452 MHz, span = [2442, 2462] MHz) + ScalarTransmissionAnalogModel outOfBandTxModel(0, 0, 1, Hz(2452000000.0), MHz(20), mW(20)); + + auto isContained = [](const BandListening& l, const INarrowbandSignalAnalogModel& s) { + auto lMin = l.getCenterFrequency() - l.getBandwidth() / 2; + auto lMax = l.getCenterFrequency() + l.getBandwidth() / 2; + auto sMin = s.getCenterFrequency() - s.getBandwidth() / 2; + auto sMax = s.getCenterFrequency() + s.getBandwidth() / 2; + return sMin >= lMin && sMax <= lMax; + }; + + ASSERT(isContained(listening40, primaryTxModel)); + ASSERT(isContained(listening40, secondaryTxModel)); + ASSERT(isContained(listening40, bondedTxModel)); + ASSERT(!isContained(listening40, outOfBandTxModel)); + + // Channel 4 (2432 MHz) HT40- (secondary below, bonded = 2422 MHz, span = [2402, 2442] MHz) + Ieee80211Channel ch4Below(band24, 4, IEEE80211_SECONDARY_CHANNEL_BELOW); + BandListening listening40Below(nullptr, 0, 1, Coord::ZERO, Coord::ZERO, ch4Below.getBondedCenterFrequency(), bondedBw); + ScalarTransmissionAnalogModel primaryTxModel4(0, 0, 1, ch4Below.getCenterFrequency(), MHz(20), mW(20)); + ScalarTransmissionAnalogModel secondaryTxModel4(0, 0, 1, ch4Below.getSecondaryCenterFrequency(), MHz(20), mW(20)); + + ASSERT(isContained(listening40Below, primaryTxModel4)); + ASSERT(isContained(listening40Below, secondaryTxModel4)); + + // 20 MHz listening on Channel 0 should reject 40 MHz bonded signal + BandListening listening20(nullptr, 0, 1, Coord::ZERO, Coord::ZERO, ch0Above.getCenterFrequency(), MHz(20)); + ASSERT(isContained(listening20, primaryTxModel)); + ASSERT(!isContained(listening20, bondedTxModel)); + ASSERT(!isContained(listening20, secondaryTxModel)); + + EV << "HT40 subchannel containment tests PASSED.\n"; +} + +EV << ".\n"; + +%contains: stdout +HT40 subchannel containment tests PASSED. +. From 09dce7566b355bc62c9684b447c438d66610101a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 14:51:10 +0200 Subject: [PATCH 82/91] Ieee80211: apportion partial spectral overlap power during HT40 CCA sub-band energy detection When using ScalarMediumAnalogModel, evaluating a 40 MHz signal across a 20 MHz sub-channel listening query triggered a cRuntimeError in computeNoise() because the signal was only partially overlapping. In Ieee80211Receiver::computeHtCcaBusy, directly compute the apportioned overlapping power for scalar medium models based on the frequency overlap fraction, and add a unit test validating sub-band power apportioning and ED detection. --- .../packetlevel/Ieee80211Receiver.cc | 32 +++++- .../Ieee80211Ht40CcaPartialOverlap_1.test | 100 ++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/unit/Ieee80211Ht40CcaPartialOverlap_1.test diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 4dbab31a01b..2868f105a52 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -8,6 +8,7 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" #include "inet/common/math/Functions.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/INarrowbandSignalAnalogModel.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" #include "inet/physicallayer/wireless/common/radio/packetlevel/BandListening.h" @@ -115,9 +116,34 @@ bool Ieee80211Receiver::computeHtCcaBusy(const IListening *listening, const IInt { const auto *bandListening = check_and_cast(listening); const auto *mediumAnalogModel = listening->getReceiverRadio()->getMedium()->getAnalogModel(); - const INoise *noise = mediumAnalogModel->computeNoise(listening, interference); - bool busy = noise->computeMaxPower(listening->getStartTime(), listening->getEndTime()) >= htCcaEnergyDetection; - delete noise; + bool busy = false; + if (dynamic_cast(mediumAnalogModel) != nullptr) { + W totalPower = W(0); + const auto *backgroundNoise = interference->getBackgroundNoise(); + if (backgroundNoise != nullptr) + totalPower += backgroundNoise->computeMaxPower(listening->getStartTime(), listening->getEndTime()); + const auto listeningMin = bandListening->getCenterFrequency() - bandListening->getBandwidth() / 2; + const auto listeningMax = bandListening->getCenterFrequency() + bandListening->getBandwidth() / 2; + for (auto reception : *interference->getInterferingReceptions()) { + const auto *signal = dynamic_cast(reception->getAnalogModel()); + if (signal != nullptr) { + const auto signalMin = signal->getCenterFrequency() - signal->getBandwidth() / 2; + const auto signalMax = signal->getCenterFrequency() + signal->getBandwidth() / 2; + const auto overlapMin = std::max(listeningMin, signalMin); + const auto overlapMax = std::min(listeningMax, signalMax); + if (overlapMin < overlapMax && signal->getBandwidth() > Hz(0)) { + double fraction = (overlapMax - overlapMin).get() / signal->getBandwidth().get(); + totalPower += signal->computeMinPower(reception->getStartTime(), reception->getEndTime()) * fraction; + } + } + } + busy = totalPower >= htCcaEnergyDetection; + } + else { + const INoise *noise = mediumAnalogModel->computeNoise(listening, interference); + busy = noise->computeMaxPower(listening->getStartTime(), listening->getEndTime()) >= htCcaEnergyDetection; + delete noise; + } if (busy) return true; diff --git a/tests/unit/Ieee80211Ht40CcaPartialOverlap_1.test b/tests/unit/Ieee80211Ht40CcaPartialOverlap_1.test new file mode 100644 index 00000000000..6a39aba5834 --- /dev/null +++ b/tests/unit/Ieee80211Ht40CcaPartialOverlap_1.test @@ -0,0 +1,100 @@ +%description: +Validate IEEE 802.11n HT40 Clear Channel Assessment (CCA) sub-band partial-overlap energy apportioning and sensitivity sensing. + +%includes: +#include "inet/common/Units.h" +#include "inet/common/math/Functions.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceptionAnalogModel.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/BandListening.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::units::values; + +static W computeApportionedPower(const BandListening& listening, const INarrowbandSignalAnalogModel& signal, W signalPower) +{ + auto listeningMin = listening.getCenterFrequency() - listening.getBandwidth() / 2; + auto listeningMax = listening.getCenterFrequency() + listening.getBandwidth() / 2; + auto signalMin = signal.getCenterFrequency() - signal.getBandwidth() / 2; + auto signalMax = signal.getCenterFrequency() + signal.getBandwidth() / 2; + auto overlapMin = std::max(listeningMin, signalMin); + auto overlapMax = std::min(listeningMax, signalMax); + if (overlapMin < overlapMax && signal.getBandwidth() > Hz(0)) { + double fraction = (overlapMax - overlapMin).get() / signal.getBandwidth().get(); + return signalPower * fraction; + } + return W(0); +} + +%activity: +{ + const IIeee80211Band *band24 = Ieee80211CompliantBands::getBand("2.4 GHz"); + ASSERT(band24 != nullptr); + + // Channel 0 (2412 MHz) HT40+ (secondary above, bonded = 2422 MHz, span = [2402, 2442] MHz) + Ieee80211Channel ch0Above(band24, 0, IEEE80211_SECONDARY_CHANNEL_ABOVE); + Hz primaryCenter = ch0Above.getCenterFrequency(); + Hz secondaryCenter = ch0Above.getSecondaryCenterFrequency(); + Hz bondedCenter = ch0Above.getBondedCenterFrequency(); + + BandListening primaryListening(nullptr, 0, 1, Coord::ZERO, Coord::ZERO, primaryCenter, MHz(20)); + BandListening secondaryListening(nullptr, 0, 1, Coord::ZERO, Coord::ZERO, secondaryCenter, MHz(20)); + + // Sensitivities per IEEE Std 802.11-2024 Clause 19.3.19.6 + W htCca20 = mW(math::dBmW2mW(-82.0)); + W htCca40 = mW(math::dBmW2mW(-79.0)); + W htCcaEd = mW(math::dBmW2mW(-62.0)); + + // 1. Bonded 40 MHz transmission at -60 dBm (span [2402, 2442] MHz) + W p40 = mW(math::dBmW2mW(-60.0)); + ScalarReceptionAnalogModel bondedSignal(0, 0, 1, bondedCenter, MHz(40), p40); + + W primaryApportioned = computeApportionedPower(primaryListening, bondedSignal, p40); + W secondaryApportioned = computeApportionedPower(secondaryListening, bondedSignal, p40); + + // Each 20 MHz slice receives 50% of the 40 MHz bonded power + ASSERT(fabs((primaryApportioned - p40 * 0.5).get()) < 1e-12); + ASSERT(fabs((secondaryApportioned - p40 * 0.5).get()) < 1e-12); + ASSERT(fabs((primaryApportioned + secondaryApportioned - p40).get()) < 1e-12); + + // Since p40 = -60 dBm, 50% power = -63.01 dBm (< htCcaEd = -62 dBm) + ASSERT(primaryApportioned < htCcaEd); + ASSERT(secondaryApportioned < htCcaEd); + + // 2. High-power 40 MHz transmission at -55 dBm -> apportioned = -58.01 dBm (>= htCcaEd) + W p40_high = mW(math::dBmW2mW(-55.0)); + ScalarReceptionAnalogModel bondedHighSignal(0, 0, 1, bondedCenter, MHz(40), p40_high); + W primaryHighApportioned = computeApportionedPower(primaryListening, bondedHighSignal, p40_high); + ASSERT(primaryHighApportioned >= htCcaEd); + + // 3. Primary 20 MHz signal (span [2402, 2422] MHz) + W p20 = mW(math::dBmW2mW(-75.0)); + ScalarReceptionAnalogModel primarySignal(0, 0, 1, primaryCenter, MHz(20), p20); + W primaryFromPrimary = computeApportionedPower(primaryListening, primarySignal, p20); + W secondaryFromPrimary = computeApportionedPower(secondaryListening, primarySignal, p20); + ASSERT(fabs((primaryFromPrimary - p20).get()) < 1e-12); + ASSERT(secondaryFromPrimary == W(0)); + + // 4. Partially overlapping 20 MHz signal on Channel 2 (center 2417 MHz, span [2407, 2427] MHz) + ScalarReceptionAnalogModel ch2Signal(0, 0, 1, Hz(2417000000.0), MHz(20), p20); + W primaryFromCh2 = computeApportionedPower(primaryListening, ch2Signal, p20); + W secondaryFromCh2 = computeApportionedPower(secondaryListening, ch2Signal, p20); + // Overlap with primary [2402, 2422]: [2407, 2422] = 15 MHz (75%) + ASSERT(fabs((primaryFromCh2 - p20 * 0.75).get()) < 1e-12); + // Overlap with secondary [2422, 2442]: [2422, 2427] = 5 MHz (25%) + ASSERT(fabs((secondaryFromCh2 - p20 * 0.25).get()) < 1e-12); + ASSERT(fabs((primaryFromCh2 + secondaryFromCh2 - p20).get()) < 1e-12); + + EV << "HT40 CCA sub-band partial-overlap power apportioning PASSED.\n"; +} + +EV << ".\n"; + +%contains: stdout +HT40 CCA sub-band partial-overlap power apportioning PASSED. +. From a00ee0407c36189bc4bcfe87cee9fe3b226d2291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 19:50:39 +0200 Subject: [PATCH 83/91] Ieee80211: scale sub-band noise in ScalarSnir and decouple primary backoff from secondary-only energy --- src/inet/linklayer/ieee80211/mac/Rx.cc | 3 ++- .../common/analogmodel/scalar/ScalarSnir.cc | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index cfe10da1a12..8ac8f2e943d 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -159,7 +159,8 @@ void Rx::recomputeMediumFree() { bool oldMediumFree = mediumFree; // note: the duration of mode switching (rx-to-tx or tx-to-rx) should also count as busy - bool primaryPhysicallyIdle = (receptionState == IRadio::RECEPTION_STATE_IDLE) && (!ht40Cca || !primaryCcaBusy); + bool primaryPhysicallyIdle = (receptionState != IRadio::RECEPTION_STATE_RECEIVING) && + (!ht40Cca ? (receptionState == IRadio::RECEPTION_STATE_IDLE) : !primaryCcaBusy); mediumFree = primaryPhysicallyIdle && transmissionState == IRadio::TRANSMISSION_STATE_UNDEFINED && !endNavTimer->isScheduled(); if (mediumFree != oldMediumFree) { for (auto contention : contentions) diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc index ec577ca7ed6..eae1b525035 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc @@ -32,18 +32,31 @@ std::ostream& ScalarSnir::printToStream(std::ostream& stream, int level, int evF return stream; } +static double computeBandwidthScale(const ScalarReceptionAnalogModel *signalModel, const ScalarNoise *scalarNoise) +{ + Hz signalBw = signalModel->getBandwidth(); + Hz noiseBw = scalarNoise->getBandwidth(); + if (signalBw > Hz(0) && noiseBw > signalBw) + return (signalBw / noiseBw).get(); + return 1.0; +} + double ScalarSnir::computeMin() const { auto scalarSignalAnalogModel = check_and_cast(reception->getAnalogModel()); const ScalarNoise *scalarNoise = check_and_cast(noise); - return (scalarSignalAnalogModel->getPower() / scalarNoise->computeMaxPower(reception->getStartTime(), reception->getEndTime())).get(); + double bwScale = computeBandwidthScale(scalarSignalAnalogModel, scalarNoise); + W effectiveNoise = scalarNoise->computeMaxPower(reception->getStartTime(), reception->getEndTime()) * bwScale; + return (scalarSignalAnalogModel->getPower() / effectiveNoise).get(); } double ScalarSnir::computeMax() const { auto scalarSignalAnalogModel = check_and_cast(reception->getAnalogModel()); const ScalarNoise *scalarNoise = check_and_cast(noise); - return (scalarSignalAnalogModel->getPower() / scalarNoise->computeMinPower(reception->getStartTime(), reception->getEndTime())).get(); + double bwScale = computeBandwidthScale(scalarSignalAnalogModel, scalarNoise); + W effectiveNoise = scalarNoise->computeMinPower(reception->getStartTime(), reception->getEndTime()) * bwScale; + return (scalarSignalAnalogModel->getPower() / effectiveNoise).get(); } double ScalarSnir::getMin() const @@ -71,12 +84,13 @@ double ScalarSnir::computeMean(simtime_t startTime, simtime_t endTime) const { auto scalarSignalAnalogModel = check_and_cast(reception->getAnalogModel()); const ScalarNoise *scalarNoise = check_and_cast(noise); + double bwScale = computeBandwidthScale(scalarSignalAnalogModel, scalarNoise); const auto& signalPowerFunction = makeShared>>(scalarSignalAnalogModel->getPower()); const auto& snirFunction = signalPowerFunction->divide(scalarNoise->getPower()); math::Point startPoint(startTime); math::Point endPoint(endTime); math::Interval interval(startPoint, endPoint, 0b1, 0b0, 0b0); - return snirFunction->getMean(interval); + return snirFunction->getMean(interval) / bwScale; } } // namespace physicallayer From 396ee59639ee7870941a5e2d16b6855206fe20c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 22:05:15 +0200 Subject: [PATCH 84/91] tests: update wireless showcase and tutorial fingerprints after HT40 channel support Updating reference fingerprints (tplx, ~tNl, ~tND) in showcases.csv and tutorials.csv following the IEEE 802.11 HT40 primary and secondary channel enhancements. The changes in PHY/MAC channel sensing, band-containment matching in receiver analog models, flat background noise PSD calculations, and CCA state snapshot notifications shifted the simulation event trajectories for scenarios using Ieee80211RadioMedium (Dimensional analog model). Affected configurations (19 total): - showcases/routing/manet: Aodv, Dsdv - showcases/visualizer/canvas: instrumentfigures (General), datalinkactivity (Dynamic), routingtable (Dynamic), statistic (PacketErrorRate) - showcases/wireless/analogmodel: Distance - showcases/wireless/blockack: NoFragmentation, Fragmentation, MixedTraffic - showcases/wireless/fragmentation: DCFnofrag, DCFfrag, HCFfrag, HCFfragblockack - showcases/wireless/power: General - showcases/wireless/ratecontrol: NoRateControl, AarfRateControl - tutorials/configurator: Step10C, Step12 All 475 wireless/802.11 fingerprint tests and 6 IEEE 802.11 unit tests pass deterministically. --- tests/fingerprint/showcases.csv | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index e9673a1dd4d..e7c344bbb21 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -39,7 +39,7 @@ /showcases/mobility/spatial/, -f omnetpp.ini -c Drone -r 0, 100s, 9475-d04c/tplx;0000-0000/~tNl;0000-0000/~tND;1ec0-fec3/tyf, PASS, # /showcases/routing/manet/, -f omnetpp.ini -c MobileNodesBase -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, PASS, # MobileNodesBase extended -/showcases/routing/manet/, -f omnetpp.ini -c Aodv -r 0, 100s, d4f1-1c35/tplx;c268-fae7/~tNl;8d69-d2c8/~tND;cfeb-1d46/tyf, PASS, wireless Ipv4 +/showcases/routing/manet/, -f omnetpp.ini -c Aodv -r 0, 100s, de53-c4fb/tplx;dc3e-91a6/~tNl;8d69-d2c8/~tND;cfeb-1d46/tyf, PASS, wireless Ipv4 /showcases/routing/manet/, -f omnetpp.ini -c Dsdv -r 0, 100s, 84f0-9aea/tplx;bdad-9a28/~tNl;fefe-9ffb/~tND;91a6-dbc7/tyf, PASS, wireless Ipv4 /showcases/routing/manet/, -f omnetpp.ini -c Gpsr -r 0, 100s, 53c9-5456/tplx;5452-5da6/~tNl;fe57-b374/tyf, PASS, wireless Ipv4 @@ -130,7 +130,7 @@ /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 +/showcases/visualizer/canvas/instrumentfigures/, -f omnetpp.ini -c General -r 0, 3s, 72e9-231a/tplx;fc5a-a987/~tNl;85f0-a9c3/~tND;e52f-124b/tyf, PASS, wireless Ipv4 /showcases/visualizer/canvas/interfacetable/, -f omnetpp.ini -c EnablingVisualization -r 0, 5s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND;0000-0000/tyf, PASS, /showcases/visualizer/canvas/interfacetable/, -f omnetpp.ini -c AdvancedFeatures -r 0, 5s, c574-efc0/tplx;518c-d31e/~tNl;03fe-e203/~tND;b2d8-b329/tyf, PASS, wireless EthernetMac 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, 78cf-be58/tplx;a1d6-4bb6/~tNl;d600-2b73/~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;aee4-3a9d/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, 3f9d-465e/tplx;78b5-09d7/~tNl;4cc7-05ed/~tND;d760-98fe/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, 98d8-d5d1/tplx;ebe5-7cf5/~tNl;9e70-fbb5/~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;ea74-dbf2/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, e9fb-242d/tplx;6719-4a98/~tNl;1fc9-fb0d/~tND, PASS, wireless Ipv4 @@ -264,9 +264,9 @@ /showcases/wireless/errorrate/, -f omnetpp.ini -c General -r '$bitrate==48 && $distance==350', 500s, 6518-1016/tplx;b21f-0ea2/~tNl;0ef5-d7e0/~tND;9b47-3a1d/tyf, PASS, wireless Ipv4 /showcases/wireless/errorrate/, -f omnetpp.ini -c General -r '$bitrate==54 && $distance==350', 500s, 7a69-d427/tplx;3aa8-584d/~tNl;e972-9ec9/~tND;6be5-e4ba/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 57ee-7ddf/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, 4b00-315e/tplx;a24a-a070/~tNl;5dae-0b8e/~tND;70a1-3cc7/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 2a95-12db/tplx;874a-556a/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 4e74-5864/tplx;e13c-fa45/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, 50c3-7877/tplx;54a7-6e16/~tNl;5dae-0b8e/~tND;70a1-3cc7/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 892b-8baa/tplx;370a-d694/~tNl;ab8b-0a8a/~tND;641e-b548/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 3623-0ff6/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -316,8 +316,8 @@ /showcases/wireless/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 26be-ff05/tplx;9c39-00a3/~tNl;03a9-2f02/~tND;5fef-ddb3/tyf, PASS, wireless Ipv4 /showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 665f-be37/tplx;805c-ea54/~tNl;d745-7405/~tND;afaf-c551/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 +/showcases/wireless/ratecontrol/, -f omnetpp.ini -c NoRateControl -r 0, 14s, a9c1-6412/tplx;3302-4675/~tNl;ec30-e32d/~tND;dad3-7f89/tyf, PASS, wireless Ipv4 +/showcases/wireless/ratecontrol/, -f omnetpp.ini -c AarfRateControl -r 0, 12s, 00bd-a83c/tplx;be0d-7457/~tNl;de90-6575/~tND;7539-d32d/tyf, PASS, wireless Ipv4 # /showcases/wireless/scaling/, -f omnetpp.ini -c PingBase -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, PASS, # PingBase extended # /showcases/wireless/scaling/, -f omnetpp.ini -c UDPBase -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, PASS, # UDPBase extended From be7e8c1e75790a9c2b74951b6e92de5056ad4f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 8 Sep 2026 01:31:38 +0200 Subject: [PATCH 85/91] tests: updated fingerprints of networkpathactivity ChangingPaths and seaport. After running the full suite of fingerprint tests (it was not carried in the previous commit), these two arised From 026152227c4c6a202d3dcedaf40bb22f1a104d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 01:18:44 +0200 Subject: [PATCH 86/91] ieee80211: keep channel geometry consistent on bandwidth changes A bandwidth-only HT40-to-HT20 command left the secondary offset and bonded receiver center in place. Normalize the target geometry and apply width before publishing channel changes so synchronous observers see a consistent configuration. Constrain AP operational width to the active PHY width providers so beacons reflect the narrower channel. Cover generic bandwidth-only commands, notification-time geometry, and advertised HT operation with a module test. --- .../ieee80211/mgmt/Ieee80211MgmtApBase.cc | 1 + .../ieee80211/packetlevel/Ieee80211Radio.cc | 30 ++-- ...ee80211Ht40BandwidthReconfiguration_1.test | 165 ++++++++++++++++++ 3 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc index 3c5a90ba0ce..bde81da7db4 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc @@ -21,6 +21,7 @@ #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" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" namespace inet { diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 59a2cbc1afc..1233fa95d1c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -144,20 +144,21 @@ void Ieee80211Radio::initialize(int stage) void Ieee80211Radio::handleUpperCommand(cMessage *message) { if (message->getKind() == RADIO_C_CONFIGURE) { - Ieee80211ConfigureRadioCommand *configureCommand = dynamic_cast(message->getControlInfo()); + ConfigureRadioCommand *configureCommand = dynamic_cast(message->getControlInfo()); + auto ieee80211Command = dynamic_cast(configureCommand); if (configureCommand != nullptr) { Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); const Ieee80211Channel *currentChannel = ieee80211Receiver->getChannel(); - const char *requestedOpMode = configureCommand->getOpMode(); + const char *requestedOpMode = ieee80211Command != nullptr ? ieee80211Command->getOpMode() : ""; std::string targetOpMode = *requestedOpMode ? requestedOpMode : this->opMode; - const Ieee80211Channel *channel = configureCommand->getChannel(); - const IIeee80211Band *bandParam = configureCommand->getBand(); + const Ieee80211Channel *channel = ieee80211Command != nullptr ? ieee80211Command->getChannel() : nullptr; + const IIeee80211Band *bandParam = ieee80211Command != nullptr ? ieee80211Command->getBand() : nullptr; const IIeee80211Band *targetBand = bandParam != nullptr ? bandParam : (channel != nullptr && channel->getBand() != nullptr) ? channel->getBand() : this->band; - const Ieee80211ModeSet *modeSetParam = configureCommand->getModeSet(); + const Ieee80211ModeSet *modeSetParam = ieee80211Command != nullptr ? ieee80211Command->getModeSet() : nullptr; const Ieee80211ModeSet *targetModeSet = modeSetParam != nullptr ? modeSetParam : *requestedOpMode ? Ieee80211ModeSet::getModeSet(requestedOpMode) : this->modeSet; - int newChannelNumber = configureCommand->getChannelNumber(); + int newChannelNumber = ieee80211Command != nullptr ? ieee80211Command->getChannelNumber() : -1; int targetChannelNumber = channel != nullptr ? channel->getChannelNumber() : newChannelNumber != -1 ? newChannelNumber : currentChannel != nullptr ? currentChannel->getChannelNumber() : -1; @@ -165,13 +166,19 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) htSecondaryChannelOffset; if (targetChannelNumber != -1 && (targetBand == nullptr || targetChannelNumber < 0 || targetChannelNumber >= targetBand->getNumChannels())) throw cRuntimeError("Invalid target 802.11 channel number %d", targetChannelNumber); - if (targetChannelNumber != -1) - (void)Ieee80211Channel(targetBand, targetChannelNumber, targetSecondaryChannelOffset).getCenterFrequency(); Hz newBandwidth = configureCommand->getBandwidth(); Hz targetBandwidth = std::isnan(newBandwidth.get()) ? ieee80211Receiver->getBandwidth() : newBandwidth; + if (targetBandwidth == MHz(20)) + targetSecondaryChannelOffset = IEEE80211_SECONDARY_CHANNEL_NONE; + if (targetChannelNumber != -1) { + Ieee80211Channel targetChannel(targetBand, targetChannelNumber, targetSecondaryChannelOffset); + (void)targetChannel.getCenterFrequency(); + if (targetSecondaryChannelOffset != IEEE80211_SECONDARY_CHANNEL_NONE) + (void)targetChannel.getBondedCenterFrequency(); + } bps newBitrate = configureCommand->getBitrate(); - const IIeee80211Mode *mode = configureCommand->getMode(); + const IIeee80211Mode *mode = ieee80211Command != nullptr ? ieee80211Command->getMode() : nullptr; const IIeee80211Mode *resolvedMode = mode; if (resolvedMode == nullptr && targetModeSet != nullptr && !std::isnan(newBitrate.get())) { if (!std::isnan(newBandwidth.get())) @@ -189,8 +196,11 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) bool publishModeSet = targetModeSet != this->modeSet || targetBand != this->band || *requestedOpMode; + // Commit width before setChannel publishes geometry to synchronous observers. + if (!std::isnan(newBandwidth.get())) + setBandwidth(targetBandwidth); if (targetChannelNumber != -1 && - (currentChannel == nullptr || targetBand != this->band || targetChannelNumber != currentChannel->getChannelNumber() || + (!std::isnan(newBandwidth.get()) || currentChannel == nullptr || targetBand != this->band || targetChannelNumber != currentChannel->getChannelNumber() || targetSecondaryChannelOffset != currentChannel->getSecondaryChannelOffset())) setChannel(new Ieee80211Channel(targetBand, targetChannelNumber, targetSecondaryChannelOffset)); else if (targetBand != this->band) diff --git a/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test b/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test new file mode 100644 index 00000000000..d1b4c43f247 --- /dev/null +++ b/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test @@ -0,0 +1,165 @@ +%description: +Verify HT40 configuration and bandwidth-only HT20 transition through the radio command path and AP beacon generation. + +%file: Test.cc +#include + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.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" + +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" + +namespace inet { +namespace ieee80211 { + +class TestIeee80211MgmtAp : public Ieee80211MgmtAp +{ + public: + std::vector> sentBeacons; + + using Ieee80211MgmtApBase::getHtOperationBand; + + protected: + virtual void start() override + { + // Suppress automatic periodic beacons so test controls frame generation. + Ieee80211MgmtApBase::start(); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, + int subtype, const MacAddress& destAddr, uint64_t transactionId = 0) override + { + if (subtype == ST_BEACON) { + auto beacon = dynamicPtrCast(body); + if (beacon != nullptr) + sentBeacons.push_back(constPtrCast(beacon)); + } + } + + public: + void emitBeaconNow() + { + Enter_Method("emitBeaconNow"); + sendBeacon(); + } + +}; +Define_Module(TestIeee80211MgmtAp); + +class CcaCheckingReceiver : public physicallayer::Ieee80211Receiver +{ + public: + using Ieee80211Receiver::isHtCcaOperation; +}; +Define_Module(CcaCheckingReceiver); + +class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener +{ + public: + bool checking = false; + int notifications = 0; + void narrow() + { + Enter_Method("narrow"); + checking = true; + subscribe(radioChannelChangedSignal, this); + auto message = new cMessage("configure", physicallayer::RADIO_C_CONFIGURE); + auto command = new physicallayer::ConfigureRadioCommand(); + command->setBandwidth(MHz(20)); + message->setControlInfo(command); + handleUpperCommand(message); + ASSERT(notifications == 1); + updateCcaState(); + ASSERT(!getCcaSnapshot().isHt40()); + } + virtual void receiveSignal(cComponent *, simsignal_t, intval_t, cObject *) override + { + if (checking) { + const auto *rx = check_and_cast(getReceiver()); + ASSERT(rx->getBandwidth() == MHz(20)); + ASSERT(rx->getCenterFrequency() == getChannel()->getCenterFrequency()); + ASSERT(getChannel()->getSecondaryChannelOffset() == physicallayer::IEEE80211_SECONDARY_CHANNEL_NONE); + notifications++; + } + } + void checkCca() + { + Enter_Method("checkCca"); + updateCcaState(); + ASSERT(getCcaSnapshot().isHt40()); + ASSERT(check_and_cast(getReceiver())->isHtCcaOperation()); + } +}; +Define_Module(ConfiguringRadio); + +class ChannelWidthTest : public cSimpleModule +{ + public: + ChannelWidthTest() : cSimpleModule(65536) {} + virtual void activity() override + { + auto radio = check_and_cast(getModuleByPath("^.ap.wlan[0].radio")); + auto mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + const auto *rx = check_and_cast(radio->getReceiver()); + ASSERT(rx->getBandwidth() == MHz(40)); + ASSERT(rx->getCenterFrequency() == radio->getChannel()->getBondedCenterFrequency()); + radio->checkCca(); + mgmt->emitBeaconNow(); + ASSERT(mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); + radio->narrow(); + ASSERT(rx->getBandwidth() == MHz(20)); + ASSERT(rx->getCenterFrequency() == radio->getChannel()->getCenterFrequency()); + mgmt->emitBeaconNow(); + ASSERT(mgmt->sentBeacons.back()->getHtOperationPresent()); + ASSERT(!mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); + ASSERT(mgmt->sentBeacons.back()->getHtOperation().secondaryChannelOffset == 0); + std::cout << "HT40 initialization, CCA, atomic HT20 transition and beacon width verified.\n"; + } +}; +Define_Module(ChannelWidthTest); +} } + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.wireless.AccessPoint; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Receiver; +module CcaCheckingReceiver extends Ieee80211Receiver { parameters: @class(::inet::ieee80211::CcaCheckingReceiver); } +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple TestIeee80211MgmtAp extends Ieee80211MgmtAp { parameters: @class(::inet::ieee80211::TestIeee80211MgmtAp); } +module ConfiguringRadio extends Ieee80211Radio { parameters: @class(::inet::ieee80211::ConfiguringRadio); } +simple ChannelWidthTest extends SimpleModule { parameters: @class(::inet::ieee80211::ChannelWidthTest); } +network ChannelWidthNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + test: ChannelWidthTest; +} +%inifile: omnetpp.ini +[General] +network = ChannelWidthNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 50us +seed-set = 0 +**.wlan[*].mgmt.typename = "TestIeee80211MgmtAp" +**.wlan[*].radio.receiver.typename = "CcaCheckingReceiver" +**.wlan[*].radio.typename = "ConfiguringRadio" +**.wlan[*].radio.channelNumber = 4 +**.wlan[*].radio.bandwidth = 40MHz +**.wlan[*].radio.htSecondaryChannelOffset = "above" +**.wlan[*].mib.htSecondaryChannelOffset = 1 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 135Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.mobility.initFromDisplayString = false +*.ap.mobility.initialX = 0m +*.ap.mobility.initialY = 0m +%contains: stdout +HT40 initialization, CCA, atomic HT20 transition and beacon width verified. From 8dbf4ccf4ba0a95fefde169de2a2c2839a8706aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 01:18:50 +0200 Subject: [PATCH 87/91] ieee80211: enable HT40 configuration for Greenfield mode sets HT40 validation and CCA activation matched the mixed-mode catalog name, rejecting valid Greenfield configurations despite their HT40 modes. Use the mode-set HT capability together with the configured channel width and secondary offset. Add a Greenfield HT40 runtime configuration test covering receiver geometry, HT subchannel CCA activation, and subsequent bandwidth-only narrowing to HT20. --- .../ieee80211/packetlevel/Ieee80211Radio.cc | 8 +- .../packetlevel/Ieee80211Receiver.cc | 2 +- ...eee80211Ht40GreenfieldConfiguration_1.test | 165 ++++++++++++++++++ 3 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 1233fa95d1c..b6ea8d5f1f8 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -70,7 +70,7 @@ void Ieee80211Radio::updateCcaState() auto channel = ieee80211Receiver == nullptr ? nullptr : ieee80211Receiver->getChannel(); bool ht40Configured = channel != nullptr && channel->getSecondaryChannelOffset() != IEEE80211_SECONDARY_CHANNEL_NONE && - modeSet != nullptr && !strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") && + modeSet != nullptr && modeSet->isHtOperationSupported() && ieee80211Receiver->getBandwidth() == MHz(40); bool ht40 = ht40Configured && isReceiverMode(radioMode); bool primaryBusy = false; @@ -116,14 +116,14 @@ void Ieee80211Radio::initialize(int stage) ieee80211Receiver->setBandwidth(radioBw); ieee80211Transmitter->setBandwidth(radioBw); } - if (modeSet != nullptr && !strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") && + if (modeSet != nullptr && modeSet->isHtOperationSupported() && radioBw == MHz(40) && htSecondaryChannelOffset == IEEE80211_SECONDARY_CHANNEL_NONE) throw cRuntimeError("HT 40 MHz operation requires a secondary channel offset of above or below"); if (htSecondaryChannelOffset != IEEE80211_SECONDARY_CHANNEL_NONE) { // IEEE Std 802.11-2024, 19.2.3 and 19.3.15.4: the secondary // channel is an HT40-only operating-channel property. - if (modeSet == nullptr || strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") || + if (modeSet == nullptr || !modeSet->isHtOperationSupported() || radioBw != MHz(40)) throw cRuntimeError("htSecondaryChannelOffset above/below requires HT 40 MHz operation"); } @@ -186,7 +186,7 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) else resolvedMode = targetModeSet->getMode(newBitrate); } - if (targetModeSet != nullptr && !strcmp(targetModeSet->getName(), "n(mixed-2.4Ghz)") && + if (targetModeSet != nullptr && targetModeSet->isHtOperationSupported() && ((targetBandwidth == MHz(40)) || (resolvedMode != nullptr && dynamic_cast(resolvedMode) != nullptr && resolvedMode->getDataMode()->getBandwidth() == MHz(40))) && diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 2868f105a52..1dff098798e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -81,7 +81,7 @@ const IListeningDecision *Ieee80211Receiver::computeListeningDecision(const ILis bool Ieee80211Receiver::isHtCcaOperation() const { - return modeSet != nullptr && !strcmp(modeSet->getName(), "n(mixed-2.4Ghz)") && + return modeSet != nullptr && modeSet->isHtOperationSupported() && channel != nullptr && (bandwidth == MHz(20) || (bandwidth == MHz(40) && channel->getSecondaryChannelOffset() != IEEE80211_SECONDARY_CHANNEL_NONE)); } diff --git a/tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test b/tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test new file mode 100644 index 00000000000..d4c5c7552b5 --- /dev/null +++ b/tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test @@ -0,0 +1,165 @@ +%description: +Verify HT40 configuration and bandwidth-only HT20 transition through the radio command path and AP beacon generation. + +%file: Test.cc +#include + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.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" + +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" + +namespace inet { +namespace ieee80211 { + +class TestIeee80211MgmtAp : public Ieee80211MgmtAp +{ + public: + std::vector> sentBeacons; + + using Ieee80211MgmtApBase::getHtOperationBand; + + protected: + virtual void start() override + { + // Suppress automatic periodic beacons so test controls frame generation. + Ieee80211MgmtApBase::start(); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, + int subtype, const MacAddress& destAddr, uint64_t transactionId = 0) override + { + if (subtype == ST_BEACON) { + auto beacon = dynamicPtrCast(body); + if (beacon != nullptr) + sentBeacons.push_back(constPtrCast(beacon)); + } + } + + public: + void emitBeaconNow() + { + Enter_Method("emitBeaconNow"); + sendBeacon(); + } + +}; +Define_Module(TestIeee80211MgmtAp); + +class CcaCheckingReceiver : public physicallayer::Ieee80211Receiver +{ + public: + using Ieee80211Receiver::isHtCcaOperation; +}; +Define_Module(CcaCheckingReceiver); + +class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener +{ + public: + bool checking = false; + int notifications = 0; + void narrow() + { + Enter_Method("narrow"); + checking = true; + subscribe(radioChannelChangedSignal, this); + auto message = new cMessage("configure", physicallayer::RADIO_C_CONFIGURE); + auto command = new physicallayer::Ieee80211ConfigureRadioCommand(); + command->setBandwidth(MHz(20)); + message->setControlInfo(command); + handleUpperCommand(message); + ASSERT(notifications == 1); + updateCcaState(); + ASSERT(!getCcaSnapshot().isHt40()); + } + virtual void receiveSignal(cComponent *, simsignal_t, intval_t, cObject *) override + { + if (checking) { + const auto *rx = check_and_cast(getReceiver()); + ASSERT(rx->getBandwidth() == MHz(20)); + ASSERT(rx->getCenterFrequency() == getChannel()->getCenterFrequency()); + ASSERT(getChannel()->getSecondaryChannelOffset() == physicallayer::IEEE80211_SECONDARY_CHANNEL_NONE); + notifications++; + } + } + void checkCca() + { + Enter_Method("checkCca"); + updateCcaState(); + ASSERT(getCcaSnapshot().isHt40()); + ASSERT(check_and_cast(getReceiver())->isHtCcaOperation()); + } +}; +Define_Module(ConfiguringRadio); + +class ChannelWidthTest : public cSimpleModule +{ + public: + ChannelWidthTest() : cSimpleModule(65536) {} + virtual void activity() override + { + auto radio = check_and_cast(getModuleByPath("^.ap.wlan[0].radio")); + auto mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + const auto *rx = check_and_cast(radio->getReceiver()); + ASSERT(rx->getBandwidth() == MHz(40)); + ASSERT(rx->getCenterFrequency() == radio->getChannel()->getBondedCenterFrequency()); + radio->checkCca(); + mgmt->emitBeaconNow(); + ASSERT(mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); + radio->narrow(); + ASSERT(rx->getBandwidth() == MHz(20)); + ASSERT(rx->getCenterFrequency() == radio->getChannel()->getCenterFrequency()); + mgmt->emitBeaconNow(); + ASSERT(mgmt->sentBeacons.back()->getHtOperationPresent()); + ASSERT(!mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); + ASSERT(mgmt->sentBeacons.back()->getHtOperation().secondaryChannelOffset == 0); + std::cout << "HT40 initialization, CCA, atomic HT20 transition and beacon width verified.\n"; + } +}; +Define_Module(ChannelWidthTest); +} } + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.wireless.AccessPoint; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Receiver; +module CcaCheckingReceiver extends Ieee80211Receiver { parameters: @class(::inet::ieee80211::CcaCheckingReceiver); } +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple TestIeee80211MgmtAp extends Ieee80211MgmtAp { parameters: @class(::inet::ieee80211::TestIeee80211MgmtAp); } +module ConfiguringRadio extends Ieee80211Radio { parameters: @class(::inet::ieee80211::ConfiguringRadio); } +simple ChannelWidthTest extends SimpleModule { parameters: @class(::inet::ieee80211::ChannelWidthTest); } +network ChannelWidthNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + test: ChannelWidthTest; +} +%inifile: omnetpp.ini +[General] +network = ChannelWidthNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 50us +seed-set = 0 +**.wlan[*].mgmt.typename = "TestIeee80211MgmtAp" +**.wlan[*].radio.receiver.typename = "CcaCheckingReceiver" +**.wlan[*].radio.typename = "ConfiguringRadio" +**.wlan[*].radio.channelNumber = 4 +**.wlan[*].radio.bandwidth = 40MHz +**.wlan[*].radio.htSecondaryChannelOffset = "above" +**.wlan[*].mib.htSecondaryChannelOffset = 1 +**.wlan[*].opMode = "n(greenfield-2.4Ghz)" +**.wlan[*].bitrate = 135Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.mobility.initFromDisplayString = false +*.ap.mobility.initialX = 0m +*.ap.mobility.initialY = 0m +%contains: stdout +HT40 initialization, CCA, atomic HT20 transition and beacon width verified. From 567d26294b4f6ac1534735468b22bf0d62e98d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 01:37:53 +0200 Subject: [PATCH 88/91] ieee80211: sense supported mixed transmissions in Greenfield CCA Greenfield mode sets can receive mixed-format HT transmissions that are not selectable transmit modes. Filtering CCA by selectable membership misses those transmissions below energy detection and can leave the secondary channel falsely idle. Use receive support for CCA admission. Extend the Greenfield module test to check both HT40 subchannels below, at, and above the sensitivity threshold with a supported mixed-format transmission. --- .../packetlevel/Ieee80211Receiver.cc | 2 +- ...eee80211Ht40GreenfieldConfiguration_1.test | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 1dff098798e..916777f8d3e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -156,7 +156,7 @@ bool Ieee80211Receiver::computeHtCcaBusy(const IListening *listening, const IInt const auto *transmission = dynamic_cast(reception->getTransmission()); const auto *signal = dynamic_cast(reception->getAnalogModel()); if (transmission == nullptr || transmission->getMode() == nullptr || signal == nullptr || - !modeSet->containsMode(transmission->getMode()) || !isBandOverlapping(bandListening, signal)) + !modeSet->supportsMode(transmission->getMode()) || !isBandOverlapping(bandListening, signal)) continue; const W signalPower = signal->computeMinPower(reception->getStartTime(), reception->getEndTime()); diff --git a/tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test b/tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test index d4c5c7552b5..b3d01269e09 100644 --- a/tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test +++ b/tests/module/Ieee80211Ht40GreenfieldConfiguration_1.test @@ -1,8 +1,14 @@ %description: -Verify HT40 configuration and bandwidth-only HT20 transition through the radio command path and AP beacon generation. +Verify a Greenfield HT40 receiver senses supported mixed-format transmissions below energy detection on both subchannels, including the sensitivity boundary, and preserves HT20 reconfiguration and beacon behavior. %file: Test.cc #include +#include "inet/common/math/Functions.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceptionAnalogModel.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/BandListening.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/Reception.h" +#include "inet/physicallayer/wireless/common/signal/Interference.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" @@ -56,6 +62,7 @@ class CcaCheckingReceiver : public physicallayer::Ieee80211Receiver { public: using Ieee80211Receiver::isHtCcaOperation; + using Ieee80211Receiver::computeListeningDecision; }; Define_Module(CcaCheckingReceiver); @@ -64,6 +71,31 @@ class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener public: bool checking = false; int notifications = 0; + void checkMixedCca() + { + Enter_Method("checkMixedCca"); + using namespace physicallayer; + auto rx = check_and_cast(getReceiver()); + auto mixedMode = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")->getMode(bps(Mbps(135)), Hz(MHz(40))); + ASSERT(!rx->getModeSet()->containsMode(mixedMode)); + ASSERT(rx->getModeSet()->supportsMode(mixedMode)); + Ieee80211Transmission transmission(this, nullptr, 0, 1, 0, 0, 1, + Coord::ZERO, Coord::ZERO, Quaternion::IDENTITY, Quaternion::IDENTITY, + nullptr, nullptr, nullptr, nullptr, nullptr, mixedMode, getChannel()); + for (double dbm : {-80.0, -79.0, -75.0}) { + auto analog = new ScalarReceptionAnalogModel(0, 0, 1, getChannel()->getBondedCenterFrequency(), + MHz(40), mW(math::dBmW2mW(dbm))); + Reception reception(this, &transmission, 0, 1, Coord::ZERO, Coord::ZERO, + Quaternion::IDENTITY, Quaternion::IDENTITY, analog); + Interference interference(nullptr, new std::vector{&reception}); + for (auto frequency : {getChannel()->getCenterFrequency(), getChannel()->getSecondaryCenterFrequency()}) { + BandListening listening(this, 0, 1, Coord::ZERO, Coord::ZERO, frequency, MHz(20)); + auto decision = rx->computeListeningDecision(&listening, &interference); + ASSERT(decision->isListeningPossible() == (dbm >= -79.0)); + delete decision; + } + } + } void narrow() { Enter_Method("narrow"); @@ -110,6 +142,7 @@ class ChannelWidthTest : public cSimpleModule ASSERT(rx->getBandwidth() == MHz(40)); ASSERT(rx->getCenterFrequency() == radio->getChannel()->getBondedCenterFrequency()); radio->checkCca(); + radio->checkMixedCca(); mgmt->emitBeaconNow(); ASSERT(mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); radio->narrow(); From 3c57090a38bb78567886251612ec42b802c75dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 01:37:59 +0200 Subject: [PATCH 89/91] ieee80211: make combined radio configuration transactional A combined HT40-to-legacy command could commit bandwidth and channel geometry before mode mapping or a synchronous mode-set participant rejected the request, leaving the radio partially reconfigured. Include geometry and opMode in the mode-set transaction, restore PHY channel snapshots and participant state on failure, and defer change notifications until commit. Preserve committed state when notification observers throw and guard against reentrant configuration changes. Extend the bandwidth reconfiguration test with mode-mapping, fixed-rate, HT-width, and late participant failures. Each rejection must preserve the original radio and PHY state without change notifications. Validation: debug build, three focused module tests, the scalar-radio legacy fingerprint for run 0, and scoped architecture checks passed. --- .../ieee80211/packetlevel/Ieee80211Radio.cc | 68 +++++++++----- .../ieee80211/packetlevel/Ieee80211Radio.h | 3 +- .../packetlevel/Ieee80211Receiver.cc | 14 +++ .../ieee80211/packetlevel/Ieee80211Receiver.h | 5 ++ .../packetlevel/Ieee80211Transmitter.cc | 14 +++ .../packetlevel/Ieee80211Transmitter.h | 5 ++ ...ee80211Ht40BandwidthReconfiguration_1.test | 88 ++++++++++++++++++- 7 files changed, 172 insertions(+), 25 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index b6ea8d5f1f8..0de1bae90f4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -143,6 +143,8 @@ void Ieee80211Radio::initialize(int stage) void Ieee80211Radio::handleUpperCommand(cMessage *message) { + if (changingModeSet) + throw cRuntimeError("Reentrant radio configuration change"); if (message->getKind() == RADIO_C_CONFIGURE) { ConfigureRadioCommand *configureCommand = dynamic_cast(message->getControlInfo()); auto ieee80211Command = dynamic_cast(configureCommand); @@ -196,24 +198,32 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) bool publishModeSet = targetModeSet != this->modeSet || targetBand != this->band || *requestedOpMode; - // Commit width before setChannel publishes geometry to synchronous observers. - if (!std::isnan(newBandwidth.get())) - setBandwidth(targetBandwidth); - if (targetChannelNumber != -1 && - (!std::isnan(newBandwidth.get()) || currentChannel == nullptr || targetBand != this->band || targetChannelNumber != currentChannel->getChannelNumber() || - targetSecondaryChannelOffset != currentChannel->getSecondaryChannelOffset())) - setChannel(new Ieee80211Channel(targetBand, targetChannelNumber, targetSecondaryChannelOffset)); - else if (targetBand != this->band) - setBand(targetBand); - this->opMode = targetOpMode; - if (publishModeSet && targetModeSet != nullptr) { - if (resolvedMode != nullptr) - setModeSetAndMode(targetModeSet, resolvedMode); - else - setModeSet(targetModeSet); - } - else if (resolvedMode != nullptr) - setMode(resolvedMode); + bool changeChannel = targetChannelNumber != -1 && + (!std::isnan(newBandwidth.get()) || currentChannel == nullptr || targetBand != this->band || + targetChannelNumber != currentChannel->getChannelNumber() || + targetSecondaryChannelOffset != currentChannel->getSecondaryChannelOffset()); + auto applyConfiguration = [&]() { + auto tx = const_cast(check_and_cast(transmitter)); + if (!std::isnan(newBandwidth.get())) + setBandwidth(targetBandwidth); + if (changeChannel) { + tx->setChannel(new Ieee80211Channel(targetBand, targetChannelNumber, targetSecondaryChannelOffset)); + ieee80211Receiver->setChannel(new Ieee80211Channel(targetBand, targetChannelNumber, targetSecondaryChannelOffset)); + htSecondaryChannelOffset = targetSecondaryChannelOffset; + } + else if (targetBand != this->band) { + tx->setBand(targetBand); + ieee80211Receiver->setBand(targetBand); + } + this->band = targetBand; + this->opMode = targetOpMode; + if (!std::isnan(configureCommand->getCenterFrequency().get())) + setCenterFrequency(configureCommand->getCenterFrequency()); + }; + if (publishModeSet || resolvedMode != nullptr || changeChannel || !std::isnan(newBandwidth.get()) || + !std::isnan(configureCommand->getCenterFrequency().get())) + changeModeSet(targetModeSet, resolvedMode, resolvedMode != nullptr, applyConfiguration, + publishModeSet, changeChannel ? targetChannelNumber : -1); } } FlatRadioBase::handleUpperCommand(message); @@ -231,7 +241,8 @@ void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const II changeModeSet(modeSet, mode, true); } -void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode) +void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode, + const std::function& applyConfiguration, bool publishModeSet, int channelNumber) { if (changingModeSet) throw cRuntimeError("Reentrant radio mode-set change"); @@ -243,7 +254,7 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 // Keep the setter dispatch below; failures after PHY mutation remain fatal. if (!explicitMode) transmitter->computeModeForModeSet(modeSet); - if (modeSetCoordinator != nullptr) + if (publishModeSet && modeSetCoordinator != nullptr) modeSetCoordinator->beginModeSetChange(modeSet); changingModeSet = true; if (explicitMode) @@ -251,10 +262,17 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 else transmitter->setModeSet(modeSet); receiver->setModeSet(modeSet); + if (applyConfiguration) + applyConfiguration(); + this->modeSet = modeSet; abortIncompatibleReception(); - if (modeSetCoordinator != nullptr) + if (channelNumber != -1 && getComponentType() != nullptr) { + Ieee80211RadioChannelChangedDetails details(band); + emit(radioChannelChangedSignal, channelNumber, &details); + } + if (publishModeSet && modeSetCoordinator != nullptr) modeSetCoordinator->completeModeSetChange(modeSet); - else if (modeSet != nullptr && getComponentType() != nullptr) + else if (publishModeSet && modeSet != nullptr && getComponentType() != nullptr) emit(modesetChangedSignal, const_cast(modeSet)); if (getComponentType() != nullptr) emit(listeningChangedSignal, 0); @@ -292,6 +310,8 @@ bool Ieee80211Radio::isHtChannelWidthSupported(Hz channelWidth) const void Ieee80211Radio::setMode(const IIeee80211Mode *mode) { + if (changingModeSet) + throw cRuntimeError("Reentrant radio configuration change"); Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); ieee80211Transmitter->setMode(mode); EV << "Changing radio mode to " << mode << endl; @@ -301,6 +321,8 @@ void Ieee80211Radio::setMode(const IIeee80211Mode *mode) void Ieee80211Radio::setBand(const IIeee80211Band *band) { + if (changingModeSet) + throw cRuntimeError("Reentrant radio configuration change"); this->band = band; Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); @@ -319,6 +341,8 @@ void Ieee80211Radio::setBand(const IIeee80211Band *band) void Ieee80211Radio::setChannel(const Ieee80211Channel *channel) { + if (changingModeSet) + throw cRuntimeError("Reentrant radio configuration change"); ASSERT(channel != nullptr); ASSERT(channel->getBand() != nullptr); Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 206d371ecb1..5f95405c421 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -48,7 +48,8 @@ class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio, pu protected: virtual void initialize(int stage) override; - void changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode); + void changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode, + const std::function& applyConfiguration = {}, bool publishModeSet = true, int channelNumber = -1); void abortIncompatibleReception(); virtual void handleUpperCommand(cMessage *message) override; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 916777f8d3e..991a10f41a0 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -203,6 +203,20 @@ void Ieee80211Receiver::setModeSet(const Ieee80211ModeSet *modeSet) this->modeSet = modeSet; } +std::function Ieee80211Receiver::saveChannelState() +{ + auto savedChannel = std::make_shared>(); + if (channel != nullptr) + savedChannel->reset(new Ieee80211Channel(*channel)); + return [this, savedChannel, oldBand = band, oldBandwidth = bandwidth, oldCenterFrequency = centerFrequency]() { + delete channel; + channel = savedChannel->release(); + band = oldBand; + bandwidth = oldBandwidth; + centerFrequency = oldCenterFrequency; + }; +} + void Ieee80211Receiver::setBand(const IIeee80211Band *band) { if (this->band != band) { diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h index 78b9a96999b..2ecc176e3b3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211RECEIVER_H #define __INET_IEEE80211RECEIVER_H +#include + #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" @@ -50,6 +52,9 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase, public IIeee80211Rec const Ieee80211ModeSet *getModeSet() const { return modeSet; } virtual void setModeSet(const Ieee80211ModeSet *modeSet); + // Captures geometry for a synchronous radio configuration transaction. + // The returned one-shot restore callback neither throws nor emits signals. + std::function saveChannelState(); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); virtual void setChannelNumber(int channelNumber); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 496a85de896..06354ad0fbb 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -114,6 +114,20 @@ void Ieee80211Transmitter::setMode(const IIeee80211Mode *mode) } } +std::function Ieee80211Transmitter::saveChannelState() +{ + auto savedChannel = std::make_shared>(); + if (channel != nullptr) + savedChannel->reset(new Ieee80211Channel(*channel)); + return [this, savedChannel, oldBand = band, oldBandwidth = bandwidth, oldCenterFrequency = centerFrequency]() { + delete channel; + channel = savedChannel->release(); + band = oldBand; + bandwidth = oldBandwidth; + centerFrequency = oldCenterFrequency; + }; +} + void Ieee80211Transmitter::setBand(const IIeee80211Band *band) { if (this->band != band) { diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index ce0e61090e5..00d24537b0c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211TRANSMITTER_H #define __INET_IEEE80211TRANSMITTER_H +#include + #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" @@ -49,6 +51,9 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase, public IIeee80 // 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); + // Captures geometry for a synchronous radio configuration transaction. + // The returned one-shot restore callback neither throws nor emits signals. + std::function saveChannelState(); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); virtual void setChannelNumber(int channelNumber); diff --git a/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test b/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test index d1b4c43f247..f6bc98943e1 100644 --- a/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test +++ b/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test @@ -1,8 +1,10 @@ %description: -Verify HT40 configuration and bandwidth-only HT20 transition through the radio command path and AP beacon generation. +Verify rejected combined HT40-to-legacy commands restore radio, PHY, and participant state without publication, followed by a successful bandwidth-only HT20 transition and AP beacon generation. %file: Test.cc #include +#include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" @@ -59,11 +61,92 @@ class CcaCheckingReceiver : public physicallayer::Ieee80211Receiver }; Define_Module(CcaCheckingReceiver); +class RejectingParticipant : public cListener, public physicallayer::IIeee80211ModeSetListener +{ + public: + const physicallayer::Ieee80211ModeSet *state = nullptr; + bool applied = false; + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return state; } + virtual std::function saveModeSetState() override + { + return [this, oldState = state]() { state = oldState; }; + } + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *target) override + { + state = target; + applied = true; + throw cRuntimeError("test participant rejection"); + } +}; + class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener { public: bool checking = false; int notifications = 0; + void rejectCombinedCommands() + { + Enter_Method("rejectCombinedCommands"); + auto tx = check_and_cast(getTransmitter()); + auto rx = check_and_cast(getReceiver()); + auto oldMode = tx->getMode(); + auto oldModeSet = modeSet; + auto oldOpMode = opMode; + auto oldBand = band; + auto oldTxFrequency = tx->getCenterFrequency(); + auto oldRxFrequency = rx->getCenterFrequency(); + auto oldChannelNumber = getChannel()->getChannelNumber(); + auto oldOffset = getChannel()->getSecondaryChannelOffset(); + auto oldTimer = receptionTimer; + RejectingParticipant participant; + participant.state = oldModeSet; + subscribe(radioChannelChangedSignal, this); + subscribe(listeningChangedSignal, this); + // Reject at mode mapping, fixed-rate selection, HT width validation, + // and finally a participant after all built-in consumers have applied. + for (int probe : {0, 1, 2, 3}) { + if (probe == 3) + getSimulation()->getSystemModule()->subscribe(modesetChangedSignal, &participant); + auto message = new cMessage("rejectedConfigure", physicallayer::RADIO_C_CONFIGURE); + auto command = new physicallayer::Ieee80211ConfigureRadioCommand(); + auto target = physicallayer::Ieee80211ModeSet::getModeSet(probe >= 2 ? "n(mixed-2.4Ghz)" : "g(mixed)"); + command->setOpMode(target->getName()); + command->setBandwidth(probe == 3 ? MHz(40) : MHz(20)); + command->setChannelNumber(oldChannelNumber + 1); + if (probe != 0) + command->setMode(target->getMode(Mbps(24))); + message->setControlInfo(command); + bool rejected = false; + try { handleUpperCommand(message); } + catch (const cRuntimeError& error) { + const char *expected[] = {"Cannot map current mode", "Unknown mode for bitrate range", + "40 MHz HT operation requires", "test participant rejection"}; + ASSERT(std::string(error.what()).find(expected[probe]) != std::string::npos); + rejected = true; + delete message; + } + ASSERT(rejected); + ASSERT(tx->getBandwidth() == MHz(40) && rx->getBandwidth() == MHz(40)); + ASSERT(tx->getCenterFrequency() == oldTxFrequency && rx->getCenterFrequency() == oldRxFrequency); + ASSERT(getChannel()->getChannelNumber() == oldChannelNumber); + ASSERT(tx->getChannel()->getChannelNumber() == oldChannelNumber); + ASSERT(getChannel()->getSecondaryChannelOffset() == oldOffset); + ASSERT(tx->getChannel()->getSecondaryChannelOffset() == oldOffset); + ASSERT(htSecondaryChannelOffset == oldOffset && band == oldBand && opMode == oldOpMode); + ASSERT(modeSet == oldModeSet && tx->getModeSet() == oldModeSet && rx->getModeSet() == oldModeSet); + ASSERT(tx->getMode() == oldMode && receptionTimer == oldTimer); + ASSERT(participant.state == oldModeSet); + for (cComponent *component = this; component != nullptr; component = component->getParentModule()) + for (auto listener : component->getLocalSignalListeners(modesetChangedSignal)) + if (auto consumer = dynamic_cast(listener)) + ASSERT(consumer->getModeSet() == oldModeSet); + ASSERT(notifications == 0); + } + ASSERT(participant.applied); + getSimulation()->getSystemModule()->unsubscribe(modesetChangedSignal, &participant); + unsubscribe(radioChannelChangedSignal, this); + unsubscribe(listeningChangedSignal, this); + } void narrow() { Enter_Method("narrow"); @@ -80,12 +163,12 @@ class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener } virtual void receiveSignal(cComponent *, simsignal_t, intval_t, cObject *) override { + notifications++; if (checking) { const auto *rx = check_and_cast(getReceiver()); ASSERT(rx->getBandwidth() == MHz(20)); ASSERT(rx->getCenterFrequency() == getChannel()->getCenterFrequency()); ASSERT(getChannel()->getSecondaryChannelOffset() == physicallayer::IEEE80211_SECONDARY_CHANNEL_NONE); - notifications++; } } void checkCca() @@ -112,6 +195,7 @@ class ChannelWidthTest : public cSimpleModule radio->checkCca(); mgmt->emitBeaconNow(); ASSERT(mgmt->sentBeacons.back()->getHtOperation().staChannelWidth40Mhz); + radio->rejectCombinedCommands(); radio->narrow(); ASSERT(rx->getBandwidth() == MHz(20)); ASSERT(rx->getCenterFrequency() == radio->getChannel()->getCenterFrequency()); From 3c7fb72b1fb2b5035ef7a8c02271f8a449e54b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 01:50:43 +0200 Subject: [PATCH 90/91] ScalarSnir: preserve interferer power for narrower receptions Scaling aggregate listening-band noise by the reception bandwidth also attenuated fully contained narrowband co-channel interferers, overstating SNIR. Retain each contribution's band through scalar noise aggregation and integrate its overlap with the desired reception before computing minimum, maximum, and mean SNIR. Add a regression for a 20 MHz reception inside a 40 MHz listening band: a 20 MHz co-channel interferer retains full power while 40 MHz background noise contributes half its power. Cover adjacent-band exclusion, time-varying interference, re-aggregation, and equal-band behavior. Validation: debug build and three focused scalar unit tests pass. The new regression fails with the previous ScalarSnir calculation. Scoped architecture and source-seal checks pass. --- .../scalar/ScalarMediumAnalogModel.cc | 19 ++- .../common/analogmodel/scalar/ScalarNoise.cc | 24 +++- .../common/analogmodel/scalar/ScalarNoise.h | 16 ++- .../common/analogmodel/scalar/ScalarSnir.cc | 23 ++-- tests/unit/ScalarSnirBandwidth_1.test | 110 ++++++++++++++++++ 5 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 tests/unit/ScalarSnirBandwidth_1.test diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc index 9ae5dfc6164..82f53d7ddfe 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.cc @@ -121,6 +121,7 @@ const INoise *ScalarMediumAnalogModel::computeNoise(const IListening *listening, simtime_t noiseStartTime = SimTime::getMaxTime(); simtime_t noiseEndTime = 0; std::map powerChanges; + std::vector powerComponents; powerChanges[math::getLowerBound()] = W(0); powerChanges[math::getUpperBound()] = W(0); auto listeningMin = commonCenterFrequency - commonBandwidth / 2; @@ -133,8 +134,11 @@ const INoise *ScalarMediumAnalogModel::computeNoise(const IListening *listening, Hz signalBandwidth = receptionAnalogModel->getBandwidth(); auto signalMin = signalCenterFrequency - signalBandwidth / 2; auto signalMax = signalCenterFrequency + signalBandwidth / 2; - if (signalMin >= listeningMin && signalMax <= listeningMax) + if (signalMin >= listeningMin && signalMax <= listeningMax) { addReception(reception, noiseStartTime, noiseEndTime, powerChanges); + auto powerFunction = makeShared>(reception->getStartTime(), reception->getEndTime(), receptionAnalogModel->getPower()); + powerComponents.push_back({signalCenterFrequency, signalBandwidth, powerFunction}); + } else if (!ignorePartialInterference && areOverlappingBands(commonCenterFrequency, commonBandwidth, signalCenterFrequency, signalBandwidth)) throw cRuntimeError("Partially interfering signals are not supported by ScalarMediumAnalogModel, enable ignorePartialInterference to avoid this error!"); } @@ -142,8 +146,11 @@ const INoise *ScalarMediumAnalogModel::computeNoise(const IListening *listening, if (scalarBackgroundNoise) { auto bgMin = scalarBackgroundNoise->getCenterFrequency() - scalarBackgroundNoise->getBandwidth() / 2; auto bgMax = scalarBackgroundNoise->getCenterFrequency() + scalarBackgroundNoise->getBandwidth() / 2; - if (bgMin >= listeningMin && bgMax <= listeningMax) + if (bgMin >= listeningMin && bgMax <= listeningMax) { addNoise(scalarBackgroundNoise, noiseStartTime, noiseEndTime, powerChanges); + const auto& backgroundComponents = scalarBackgroundNoise->getPowerComponents(); + powerComponents.insert(powerComponents.end(), backgroundComponents.begin(), backgroundComponents.end()); + } else if (!ignorePartialInterference && areOverlappingBands(commonCenterFrequency, commonBandwidth, scalarBackgroundNoise->getCenterFrequency(), scalarBackgroundNoise->getBandwidth())) throw cRuntimeError("Partially interfering background noise is not supported by ScalarMediumAnalogModel, enable ignorePartialInterference to avoid this error!"); } @@ -156,12 +163,16 @@ const INoise *ScalarMediumAnalogModel::computeNoise(const IListening *listening, } EV_TRACE << "Noise power end" << endl; const auto& powerFunction = makeShared>(powerChanges, &math::LeftInterpolator::singleton); - return new ScalarNoise(noiseStartTime, noiseEndTime, commonCenterFrequency, commonBandwidth, powerFunction); + return new ScalarNoise(noiseStartTime, noiseEndTime, commonCenterFrequency, commonBandwidth, powerFunction, powerComponents); } const INoise *ScalarMediumAnalogModel::computeNoise(const IReception *reception, const INoise *noise) const { auto scalarNoise = check_and_cast(noise); + auto powerComponents = scalarNoise->getPowerComponents(); + auto receptionAnalogModel = check_and_cast(reception->getAnalogModel()); + auto receptionPowerFunction = makeShared>(reception->getStartTime(), reception->getEndTime(), receptionAnalogModel->getPower()); + powerComponents.push_back({receptionAnalogModel->getCenterFrequency(), receptionAnalogModel->getBandwidth(), receptionPowerFunction}); simtime_t noiseStartTime = SimTime::getMaxTime(); simtime_t noiseEndTime = 0; std::map powerChanges; @@ -175,7 +186,7 @@ const INoise *ScalarMediumAnalogModel::computeNoise(const IReception *reception, it.second = power; } const auto& powerFunction = makeShared>(powerChanges, &math::LeftInterpolator::singleton); - return new ScalarNoise(noiseStartTime, noiseEndTime, scalarNoise->getCenterFrequency(), scalarNoise->getBandwidth(), powerFunction); + return new ScalarNoise(noiseStartTime, noiseEndTime, scalarNoise->getCenterFrequency(), scalarNoise->getBandwidth(), powerFunction, powerComponents); } const ISnir *ScalarMediumAnalogModel::computeSNIR(const IReception *reception, const INoise *noise) const diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.cc index 51af0d5f121..6ce4eae69b4 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.cc @@ -7,13 +7,16 @@ #include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.h" +#include "inet/common/math/Functions.h" + namespace inet { namespace physicallayer { -ScalarNoise::ScalarNoise(simtime_t startTime, simtime_t endTime, Hz centerFrequency, Hz bandwidth, Ptr>> powerFunction) : +ScalarNoise::ScalarNoise(simtime_t startTime, simtime_t endTime, Hz centerFrequency, Hz bandwidth, Ptr>> powerFunction, const std::vector& powerComponents) : NarrowbandNoiseBase(startTime, endTime, centerFrequency, bandwidth), - powerFunction(powerFunction) + powerFunction(powerFunction), + powerComponents(powerComponents.empty() ? std::vector{{centerFrequency, bandwidth, powerFunction}} : powerComponents) { } @@ -25,6 +28,23 @@ std::ostream& ScalarNoise::printToStream(std::ostream& stream, int level, int ev return NarrowbandNoiseBase::printToStream(stream, level); } +Ptr>> ScalarNoise::getPower(Hz centerFrequency, Hz bandwidth) const +{ + if (centerFrequency == this->centerFrequency && bandwidth == this->bandwidth) + return powerFunction; + Ptr>> result = makeShared>>(W(0)); + for (const auto& component : powerComponents) { + auto lower = std::max(centerFrequency - bandwidth / 2, component.centerFrequency - component.bandwidth / 2); + auto upper = std::min(centerFrequency + bandwidth / 2, component.centerFrequency + component.bandwidth / 2); + if (upper > lower) { + double scale = ((upper - lower) / component.bandwidth).get(); + auto scaleFunction = makeShared>>(scale); + result = result->add(component.powerFunction->multiply(scaleFunction)); + } + } + return result; +} + W ScalarNoise::computeMinPower(simtime_t startTime, simtime_t endTime) const { math::Point startPoint(startTime); diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.h b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.h index 3be1f8d43b7..0b2cd6c2615 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.h +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarNoise.h @@ -18,15 +18,29 @@ namespace physicallayer { class INET_API ScalarNoise : public NarrowbandNoiseBase { + public: + // Each contribution has uniform spectral density within its own band. + struct PowerComponent { + Hz centerFrequency; + Hz bandwidth; + Ptr>> powerFunction; + }; + protected: Ptr>> powerFunction; + const std::vector powerComponents; public: - ScalarNoise(simtime_t startTime, simtime_t endTime, Hz centerFrequency, Hz bandwidth, Ptr>> powerFunction); + // Without explicit components, powerFunction is uniform over the given band. + ScalarNoise(simtime_t startTime, simtime_t endTime, Hz centerFrequency, Hz bandwidth, Ptr>> powerFunction, const std::vector& powerComponents = {}); virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override; virtual Ptr>> getPower() const { return powerFunction; } + const std::vector& getPowerComponents() const { return powerComponents; } + // Integrate each flat spectral contribution over the requested band before summing. + virtual Ptr>> getPower(Hz centerFrequency, Hz bandwidth) const; + virtual W computeMinPower(simtime_t startTime, simtime_t endTime) const override; virtual W computeMaxPower(simtime_t startTime, simtime_t endTime) const override; }; diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc index eae1b525035..33ab6fa196e 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.cc @@ -32,21 +32,12 @@ std::ostream& ScalarSnir::printToStream(std::ostream& stream, int level, int evF return stream; } -static double computeBandwidthScale(const ScalarReceptionAnalogModel *signalModel, const ScalarNoise *scalarNoise) -{ - Hz signalBw = signalModel->getBandwidth(); - Hz noiseBw = scalarNoise->getBandwidth(); - if (signalBw > Hz(0) && noiseBw > signalBw) - return (signalBw / noiseBw).get(); - return 1.0; -} - double ScalarSnir::computeMin() const { auto scalarSignalAnalogModel = check_and_cast(reception->getAnalogModel()); const ScalarNoise *scalarNoise = check_and_cast(noise); - double bwScale = computeBandwidthScale(scalarSignalAnalogModel, scalarNoise); - W effectiveNoise = scalarNoise->computeMaxPower(reception->getStartTime(), reception->getEndTime()) * bwScale; + auto noisePowerFunction = scalarNoise->getPower(scalarSignalAnalogModel->getCenterFrequency(), scalarSignalAnalogModel->getBandwidth()); + W effectiveNoise = noisePowerFunction->getMax(math::Interval(reception->getStartTime(), reception->getEndTime(), 0b1, 0b1, 0b0)); return (scalarSignalAnalogModel->getPower() / effectiveNoise).get(); } @@ -54,8 +45,8 @@ double ScalarSnir::computeMax() const { auto scalarSignalAnalogModel = check_and_cast(reception->getAnalogModel()); const ScalarNoise *scalarNoise = check_and_cast(noise); - double bwScale = computeBandwidthScale(scalarSignalAnalogModel, scalarNoise); - W effectiveNoise = scalarNoise->computeMinPower(reception->getStartTime(), reception->getEndTime()) * bwScale; + auto noisePowerFunction = scalarNoise->getPower(scalarSignalAnalogModel->getCenterFrequency(), scalarSignalAnalogModel->getBandwidth()); + W effectiveNoise = noisePowerFunction->getMin(math::Interval(reception->getStartTime(), reception->getEndTime(), 0b1, 0b1, 0b0)); return (scalarSignalAnalogModel->getPower() / effectiveNoise).get(); } @@ -84,13 +75,13 @@ double ScalarSnir::computeMean(simtime_t startTime, simtime_t endTime) const { auto scalarSignalAnalogModel = check_and_cast(reception->getAnalogModel()); const ScalarNoise *scalarNoise = check_and_cast(noise); - double bwScale = computeBandwidthScale(scalarSignalAnalogModel, scalarNoise); + auto noisePowerFunction = scalarNoise->getPower(scalarSignalAnalogModel->getCenterFrequency(), scalarSignalAnalogModel->getBandwidth()); const auto& signalPowerFunction = makeShared>>(scalarSignalAnalogModel->getPower()); - const auto& snirFunction = signalPowerFunction->divide(scalarNoise->getPower()); + const auto& snirFunction = signalPowerFunction->divide(noisePowerFunction); math::Point startPoint(startTime); math::Point endPoint(endTime); math::Interval interval(startPoint, endPoint, 0b1, 0b0, 0b0); - return snirFunction->getMean(interval) / bwScale; + return snirFunction->getMean(interval); } } // namespace physicallayer diff --git a/tests/unit/ScalarSnirBandwidth_1.test b/tests/unit/ScalarSnirBandwidth_1.test new file mode 100644 index 00000000000..5f89fdbb8c5 --- /dev/null +++ b/tests/unit/ScalarSnirBandwidth_1.test @@ -0,0 +1,110 @@ +%description: +ScalarMediumAnalogModel must preserve each noise contributor's spectrum for +ScalarSnir. A 20 MHz desired signal in a 40 MHz listening band sees all of a +20 MHz co-channel interferer's power and half of the 40 MHz background power. +Check min/max/mean, time variation, adjacent bands, and reception re-aggregation. + +%includes: +#include +#include "inet/common/math/Functions.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarMediumAnalogModel.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceptionAnalogModel.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSnir.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/BandListening.h" +#include "inet/physicallayer/wireless/common/signal/Interference.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +// Only the reception's time interval and analog model enter these computations. +class TestReception : public IReception +{ + private: + simtime_t startTime; + simtime_t endTime; + ScalarReceptionAnalogModel analogModel; + + public: + TestReception(simtime_t startTime, simtime_t endTime, Hz centerFrequency, Hz bandwidth, W power) : + startTime(startTime), endTime(endTime), + analogModel(0, 0, endTime - startTime, centerFrequency, bandwidth, power) {} + virtual const simtime_t getStartTime() const override { return startTime; } + virtual const simtime_t getEndTime() const override { return endTime; } + virtual const IReceptionAnalogModel *getAnalogModel() const override { return &analogModel; } + virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream; } + virtual const IRadio *getReceiverRadio() const override { throw cRuntimeError("Unused reception API"); } + virtual const ITransmission *getTransmission() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getStartTime(IRadioSignal::SignalPart part) const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getEndTime(IRadioSignal::SignalPart part) const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getPreambleStartTime() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getPreambleEndTime() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getHeaderStartTime() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getHeaderEndTime() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getDataStartTime() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getDataEndTime() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getDuration() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getDuration(IRadioSignal::SignalPart part) const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getPreambleDuration() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getHeaderDuration() const override { throw cRuntimeError("Unused reception API"); } + virtual const simtime_t getDataDuration() const override { throw cRuntimeError("Unused reception API"); } + virtual const Coord& getStartPosition() const override { throw cRuntimeError("Unused reception API"); } + virtual const Coord& getEndPosition() const override { throw cRuntimeError("Unused reception API"); } + virtual const Quaternion& getStartOrientation() const override { throw cRuntimeError("Unused reception API"); } + virtual const Quaternion& getEndOrientation() const override { throw cRuntimeError("Unused reception API"); } +}; + +void checkSnir(const ISnir *snir, double minimum, double maximum, double mean) +{ + ASSERT(std::abs(snir->getMin() - minimum) < 1e-12); + ASSERT(std::abs(snir->getMax() - maximum) < 1e-12); + ASSERT(std::abs(snir->getMean() - mean) < 1e-12); +} + +%activity: +ScalarMediumAnalogModel medium; +BandListening listening(nullptr, 0, 4, Coord::ZERO, Coord::ZERO, MHz(5200), MHz(40)); +TestReception desired(1, 3, MHz(5190), MHz(20), W(10)); +TestReception cochannel(0, 4, MHz(5190), MHz(20), W(2)); +TestReception adjacent(0, 4, MHz(5210), MHz(20), W(8)); +TestReception intermittent(2, 4, MHz(5190), MHz(20), W(2)); +auto backgroundPower = makeShared>(0, 4, W(4)); + +for (const IReception *interferer : {&cochannel, &adjacent, &intermittent}) { + Interference interference(new ScalarNoise(0, 4, MHz(5200), MHz(40), backgroundPower), + new std::vector{interferer}); + std::unique_ptr noise(medium.computeNoise(&listening, &interference)); + std::unique_ptr snir(medium.computeSNIR(&desired, noise.get())); + if (interferer == &cochannel) { + // Desired-band noise = 2 W + 4 W * 20/40 = 4 W, not 3 W. + checkSnir(snir.get(), 2.5, 2.5, 2.5); + ASSERT(check_and_cast(noise.get())->computeMaxPower(1, 3) == W(6)); + } + else if (interferer == &adjacent) + checkSnir(snir.get(), 5, 5, 5); + else { + // Equal time at SNIR 5 and 2.5; mean SNIR is 3.75. + checkSnir(snir.get(), 2.5, 5, 3.75); + auto scalarSnir = check_and_cast(snir.get()); + ASSERT(std::abs(scalarSnir->computeMean(1, 2) - 5) < 1e-12); + ASSERT(std::abs(scalarSnir->computeMean(2, 3) - 2.5) < 1e-12); + } +} + +// The other aggregation overload must also keep the individual spectra alive. +Interference background(new ScalarNoise(0, 4, MHz(5200), MHz(40), backgroundPower), + new std::vector); +std::unique_ptr backgroundNoise(medium.computeNoise(&listening, &background)); +std::unique_ptr combined(medium.computeNoise(&cochannel, backgroundNoise.get())); +backgroundNoise.reset(); +std::unique_ptr combinedSnir(medium.computeSNIR(&desired, combined.get())); +checkSnir(combinedSnir.get(), 2.5, 2.5, 2.5); + +// Equal listening and reception bands retain the full aggregate power. +TestReception wideDesired(1, 3, MHz(5200), MHz(40), W(12)); +std::unique_ptr wideSnir(medium.computeSNIR(&wideDesired, combined.get())); +checkSnir(wideSnir.get(), 2, 2, 2); +EV << "Scalar SNIR preserves per-contributor bandwidth.\n"; + +%contains: stdout +Scalar SNIR preserves per-contributor bandwidth. From ebb1dbf0616556275b042211255b469a1584ebbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 21 Sep 2026 15:57:05 +0200 Subject: [PATCH 91/91] ieee80211: adapt HT40 reconfiguration to upstream ownership Keep AP operation width aligned with the configured radio through the upstream radio contract. Adapt HT40 rejection coverage to the coordinator's fatal post-begin failure contract; the existing mode-set failure test covers that boundary. --- .../ieee80211/mgmt/Ieee80211MgmtApBase.cc | 6 +++- .../ieee80211/packetlevel/Ieee80211Radio.cc | 4 ++- .../ieee80211/packetlevel/Ieee80211Radio.h | 5 ++- ...ee80211Ht40BandwidthReconfiguration_1.test | 36 +++---------------- 4 files changed, 17 insertions(+), 34 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc index bde81da7db4..1553c29d104 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc @@ -21,7 +21,6 @@ #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" -#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" namespace inet { @@ -83,6 +82,11 @@ void Ieee80211MgmtApBase::prepareLocalOperation() const auto *band = getHtOperationBand(); band->getStandardChannelNumber(channel); auto operation = computeLocalHtOperation(channel, band); + const auto *radioContract = check_and_cast(radio); + if (!radioContract->isHtChannelWidthSupported(MHz(40))) { + operation.secondaryChannelOffset = 0; + operation.operatingChannelWidth = MHz(20); + } mib->commitBss(mib->getBssData().ssid, mib->address, band, channel, &operation); } else { diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 0de1bae90f4..cf03ed40a12 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -107,7 +107,9 @@ void Ieee80211Radio::initialize(int stage) if (stage == INITSTAGE_PHYSICAL_LAYER) { const char *bandName = par("bandName"); setBand(*bandName ? Ieee80211CompliantBands::getBand(bandName) : nullptr); - setModeSet(*opMode.c_str() ? Ieee80211ModeSet::getModeSet(opMode.c_str()) : nullptr); + // PHY submodules prepare their mode sets during local initialization. + // The coordinator is available only for runtime reconfiguration. + modeSet = check_and_cast(receiver)->getModeSet(); htSecondaryChannelOffset = Ieee80211Channel::parseSecondaryChannelOffset(par("htSecondaryChannelOffset")); Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 5f95405c421..bb8f2220b5b 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -67,7 +67,10 @@ class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio, pu public: Ieee80211Radio(); -class INET_API Ieee80211Radio : public FlatRadioBase, public IIeee80211Radio, public IIeee80211CcaProvider + virtual const Ieee80211CcaSnapshot& getCcaSnapshot() const override { return *ccaSnapshot; } + + // 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; diff --git a/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test b/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test index f6bc98943e1..dc24019cd0f 100644 --- a/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test +++ b/tests/module/Ieee80211Ht40BandwidthReconfiguration_1.test @@ -1,5 +1,5 @@ %description: -Verify rejected combined HT40-to-legacy commands restore radio, PHY, and participant state without publication, followed by a successful bandwidth-only HT20 transition and AP beacon generation. +Verify invalid combined HT40 commands are rejected before mutation, followed by a successful bandwidth-only HT20 transition and AP beacon generation. Failures after coordinator begin are fatal and covered by Ieee80211ModeSetFailure_1. %file: Test.cc #include @@ -61,24 +61,6 @@ class CcaCheckingReceiver : public physicallayer::Ieee80211Receiver }; Define_Module(CcaCheckingReceiver); -class RejectingParticipant : public cListener, public physicallayer::IIeee80211ModeSetListener -{ - public: - const physicallayer::Ieee80211ModeSet *state = nullptr; - bool applied = false; - virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return state; } - virtual std::function saveModeSetState() override - { - return [this, oldState = state]() { state = oldState; }; - } - virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *target) override - { - state = target; - applied = true; - throw cRuntimeError("test participant rejection"); - } -}; - class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener { public: @@ -98,21 +80,16 @@ class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener auto oldChannelNumber = getChannel()->getChannelNumber(); auto oldOffset = getChannel()->getSecondaryChannelOffset(); auto oldTimer = receptionTimer; - RejectingParticipant participant; - participant.state = oldModeSet; subscribe(radioChannelChangedSignal, this); subscribe(listeningChangedSignal, this); - // Reject at mode mapping, fixed-rate selection, HT width validation, - // and finally a participant after all built-in consumers have applied. - for (int probe : {0, 1, 2, 3}) { - if (probe == 3) - getSimulation()->getSystemModule()->subscribe(modesetChangedSignal, &participant); + // These validation failures precede the fatal coordinator boundary. + for (int probe : {0, 2}) { auto message = new cMessage("rejectedConfigure", physicallayer::RADIO_C_CONFIGURE); auto command = new physicallayer::Ieee80211ConfigureRadioCommand(); auto target = physicallayer::Ieee80211ModeSet::getModeSet(probe >= 2 ? "n(mixed-2.4Ghz)" : "g(mixed)"); command->setOpMode(target->getName()); command->setBandwidth(probe == 3 ? MHz(40) : MHz(20)); - command->setChannelNumber(oldChannelNumber + 1); + command->setChannelNumber(probe == 2 ? -2 : oldChannelNumber + 1); if (probe != 0) command->setMode(target->getMode(Mbps(24))); message->setControlInfo(command); @@ -120,7 +97,7 @@ class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener try { handleUpperCommand(message); } catch (const cRuntimeError& error) { const char *expected[] = {"Cannot map current mode", "Unknown mode for bitrate range", - "40 MHz HT operation requires", "test participant rejection"}; + "Invalid target 802.11 channel number", "test participant rejection"}; ASSERT(std::string(error.what()).find(expected[probe]) != std::string::npos); rejected = true; delete message; @@ -135,15 +112,12 @@ class ConfiguringRadio : public physicallayer::Ieee80211Radio, public cListener ASSERT(htSecondaryChannelOffset == oldOffset && band == oldBand && opMode == oldOpMode); ASSERT(modeSet == oldModeSet && tx->getModeSet() == oldModeSet && rx->getModeSet() == oldModeSet); ASSERT(tx->getMode() == oldMode && receptionTimer == oldTimer); - ASSERT(participant.state == oldModeSet); for (cComponent *component = this; component != nullptr; component = component->getParentModule()) for (auto listener : component->getLocalSignalListeners(modesetChangedSignal)) if (auto consumer = dynamic_cast(listener)) ASSERT(consumer->getModeSet() == oldModeSet); ASSERT(notifications == 0); } - ASSERT(participant.applied); - getSimulation()->getSystemModule()->unsubscribe(modesetChangedSignal, &participant); unsubscribe(radioChannelChangedSignal, this); unsubscribe(listeningChangedSignal, this); }