diff --git a/WHATSNEW b/WHATSNEW index 1a61a1b6935..c41301f7546 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -280,6 +280,192 @@ 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 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. + +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. + 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 + 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. + +18. Block Ack DELBA agreement ownership + + 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 + 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 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 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 + IOriginatorBlockAckAgreementPolicy must replace computeAddbaFailureTimeout() + with getAddbaResponseTimeout() and implement computeAddbaRetryBackoff(). + +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 + 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. + +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 + 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. + +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. + +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. + +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. + +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. + +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. + +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. + +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. + +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. + +29. Malformed Peer Association IDs + + Malformed peer association IDs now mark the decoded management header incorrect + instead of terminating the simulation during deserialization. + +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 + request's initial capability. + +31. 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. 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: 1. IEEE 802.11 per-station rate statistics @@ -334,6 +520,58 @@ 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. 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. + + 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. + + 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. + + 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 + 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. + + 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 -------------------------------------- @@ -453,6 +691,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/doc/project/design/ieee80211-model-architecture.md b/doc/project/design/ieee80211-model-architecture.md index 1a8f75f9199..28ce6c8deeb 100644 --- a/doc/project/design/ieee80211-model-architecture.md +++ b/doc/project/design/ieee80211-model-architecture.md @@ -92,3 +92,44 @@ 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.** `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. + +`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. + +**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 668ccf48149..005ffd2e22a 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -4,6 +4,207 @@ 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 ``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 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``, +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. + +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. + +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. + +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. + +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. + +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. + +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. + +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. + +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. + +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. + +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. + +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 ------------------------------------------ @@ -60,6 +261,140 @@ 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 and Management Hooks +---------------------------------------------------- + +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. + +``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 +------------------------------ + +``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: + +.. 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. + +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. + +``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 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, +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 +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/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/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/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/plan/done/80211htcapop-refactor-v3-evidence.md b/plan/done/80211htcapop-refactor-v3-evidence.md new file mode 100644 index 00000000000..d6d8b083689 --- /dev/null +++ b/plan/done/80211htcapop-refactor-v3-evidence.md @@ -0,0 +1,271 @@ +# 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. +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/plan/done/ht-gi-devin-comment-closure.md b/plan/done/ht-gi-devin-comment-closure.md new file mode 100644 index 00000000000..c4a8ff531a0 --- /dev/null +++ b/plan/done/ht-gi-devin-comment-closure.md @@ -0,0 +1,136 @@ +# HT/GI Devin comment closure + +Status: completed, with the pre-existing full-interface gate findings recorded below. + +Base: `dffd66845707303f661ef148e9bca2c3cca94262`. Scope: the five comments assessed in this session. + +## 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. + +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. diff --git a/plan/done/pr-1148-resolve-audit-findings.md b/plan/done/pr-1148-resolve-audit-findings.md new file mode 100644 index 00000000000..a3a9128cf44 --- /dev/null +++ b/plan/done/pr-1148-resolve-audit-findings.md @@ -0,0 +1,295 @@ +# Resolve the audit findings of PR #1148 + +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`, +`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. +- [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. + +## 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. +- [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. +- [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). +- [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. +- [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. +- [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. +- [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. + +## 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$' +``` + +- [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. +- [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). +- [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. +- [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. +- [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. +- [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. + +## Historical 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. + +## 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 +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. + +## 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. 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); diff --git a/src/inet/common/Simsignals.cc b/src/inet/common/Simsignals.cc index 126e6b6ea7e..c588aa820f5 100644 --- a/src/inet/common/Simsignals.cc +++ b/src/inet/common/Simsignals.cc @@ -23,6 +23,7 @@ simsignal_t linkBrokenSignal = cComponent::registerSignal("linkBroken"); simsignal_t modesetChangedSignal = cComponent::registerSignal("modesetChanged"); + simsignal_t interpacketGapStartedSignal = cComponent::registerSignal("interpacketGapStarted"); simsignal_t interpacketGapEndedSignal = cComponent::registerSignal("interpacketGapEnded"); @@ -153,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 69a1363fe52..b6b20c0f461 100644 --- a/src/inet/common/Simsignals.h +++ b/src/inet/common/Simsignals.h @@ -32,6 +32,7 @@ extern INET_API simsignal_t // admin modesetChangedSignal, + interpacketGapStartedSignal, interpacketGapEndedSignal, @@ -152,4 +153,3 @@ void printSignalBanner(simsignal_t signalID, intval_t value, const cObject *deta } // namespace inet #endif - diff --git a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned index 83b958c0f9f..57161c6354f 100644 --- a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned +++ b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned @@ -27,7 +27,7 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface { parameters: string energySourceModule = default(""); - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); double bitrate @unit(bps) = default(-1bps); **.opMode = this.opMode; **.bitrate = this.bitrate; @@ -92,4 +92,3 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface classifier.in <-- { @display("m=n"); } <-- tap.lowerLayerOut; } - diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned index e32eaba59fc..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. @@ -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/__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/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/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index d87f581231f..3741992872e 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,10 +27,11 @@ #include "inet/linklayer/ieee80211/mac/contract/IRx.h" #include "inet/linklayer/ieee80211/mac/contract/ITx.h" #include "inet/networklayer/contract/IInterfaceTable.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" -#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" +#include "inet/physicallayer/wireless/ieee80211/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 { @@ -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(); @@ -71,21 +74,7 @@ void Ieee80211Mac::initialize(int stage) ds = check_and_cast(getSubmodule("ds")); rx = check_and_cast(getSubmodule("rx")); tx = check_and_cast(getSubmodule("tx")); - int operationalHtSpatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), - modeSet->getMaximumNumberOfSpatialStreams()); - std::set operationalChannelWidths; - if (modeSet->isHtOperationSupported()) { - const auto *transmitter = dynamic_cast(radio->getTransmitter()); - const auto *receiver = dynamic_cast(radio->getReceiver()); - if (transmitter == nullptr || receiver == nullptr) - throw cRuntimeError("HT operation requires Ieee80211Transmitter and Ieee80211Receiver"); - for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) - if (transmitter->isHtChannelWidthSupported(channelWidth) && - receiver->isHtChannelWidthSupported(channelWidth)) - operationalChannelWidths.insert(channelWidth); - } - mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); - emit(modesetChangedSignal, modeSet); + prepareLocalCapabilities(); if (isUp()) initializeRadioMode(); rx = check_and_cast(getSubmodule("rx")); @@ -95,6 +84,130 @@ 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() +{ + Enter_Method("prepareLocalCapabilities"); + if (mib->hasPreparedLocalCapabilities()) + return; + updateLocalHtCapabilities(); +} + +void Ieee80211Mac::updateLocalHtCapabilities(bool reconfiguration) +{ + updateLocalVhtCapabilities(); + if (!modeSet->isHtOperationSupported()) { + if (reconfiguration) + mib->reconfigureLocalHtCapabilities(Ieee80211HtCapabilities(), false); + else + 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.greenfield = modeSet->isHtGreenfieldSupported(); + 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"); + + if (reconfiguration) + mib->reconfigureLocalHtCapabilities(localHtCapabilities, true); + else + 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() @@ -164,8 +277,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 +286,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 +296,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 +372,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 +413,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()); } @@ -346,6 +459,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) { @@ -393,6 +583,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 1114deaed00..b06066a4861 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -11,16 +11,20 @@ #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" #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" #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 { @@ -35,13 +39,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 +class INET_API Ieee80211Mac : public MacProtocolBase, public IIeee80211MacConfiguration, public IManagementFrameTransactionHandler, + 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; @@ -65,8 +80,11 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; virtual void initializeRadioMode(); + void updateLocalHtCapabilities(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; using MacProtocolBase::receiveSignal; virtual void configureRadioMode(physicallayer::IRadio::RadioMode radioMode); virtual void configureNetworkInterface() override; @@ -100,6 +118,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; @@ -107,6 +128,8 @@ class INET_API Ieee80211Mac : public MacProtocolBase 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); }; @@ -115,4 +138,3 @@ class INET_API Ieee80211Mac : public MacProtocolBase } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned index 51f4198660a..319581a53f8 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,25 +76,26 @@ 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; - 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"); int mtu @unit(B) = default(2304B); bool qosStation = default(false); + **.modeSetModule = default(absPath(".")); *.mibModule = default(absPath(this.mibModule)); *.rxModule = "^.rx"; *.txModule = "^.tx"; @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[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); @@ -135,4 +138,3 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac @display("p=250,200"); } } - diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index 2a482cded2f..7c63f000c2c 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 @@ -664,8 +679,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; } @@ -684,7 +698,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/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/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/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index 2d06484c6a4..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 @@ -56,38 +68,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 = computeSerializedAmsduLength(aMsduLength, dataPacket, dataHeader, dataTrailer); + 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/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 919bd813a6f..41c813a8a6c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -8,20 +8,31 @@ #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 { -BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid) : +BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) : originatorAddress(originatorAddress), - tid(tid) + tid(tid), + startingSequenceNumber(startingSequenceNumber) { } -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), 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 (!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 (BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, sequenceNumber)) + advanceStartingSequenceNumber(BlockAckWindow::getStartingSequenceNumber(sequenceNumber, windowSize)); acknowledgmentState[SequenceControlField(sequenceNumber.get(), fragmentNumber)] = true; } @@ -30,29 +41,31 @@ 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)) || BlockAckWindow::isBefore(sequenceNumber, startingSequenceNumber); } -void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) +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)) || 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 (!BlockAckWindow::isAfter(newStartingSequenceNumber, startingSequenceNumber)) + return; auto it = acknowledgmentState.begin(); while (it != acknowledgmentState.end()) { - if (SequenceNumberCyclic(it->first.getSequenceNumber()) < sequenceNumber) + if (BlockAckWindow::isBefore(SequenceNumberCyclic(it->first.getSequenceNumber()), newStartingSequenceNumber)) it = acknowledgmentState.erase(it); else it++; } + startingSequenceNumber = newStartingSequenceNumber; } } /* 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..c4bb3783ceb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -24,22 +24,24 @@ 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); + void dataFrameReceived(const Ptr& header, int windowSize); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); - void removeAckStates(SequenceNumberCyclic sequenceNumber); + bool getCompressedAckState(SequenceNumberCyclic sequenceNumber); + void advanceStartingSequenceNumber(SequenceNumberCyclic startingSequenceNumber); MacAddress getOriginatorAddress() { return originatorAddress; } Tid getTid() { return tid; } + SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } }; } /* namespace ieee80211 */ } /* namespace inet */ #endif - 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/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/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/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h new file mode 100644 index 00000000000..b46c18cbb36 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -0,0 +1,80 @@ +// +// 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" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.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; +} + +// 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, 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 && + basicBlockAck->getStartingSequenceNumber() == blockAckReqDetails.startingSequenceNumber; + } + else { + auto compressedBlockAck = dynamicPtrCast(blockAck); + return compressedBlockAck != nullptr && compressedBlockAck->getTidInfo() == blockAckReqDetails.tid && + compressedBlockAck->getStartingSequenceNumber() == blockAckReqDetails.startingSequenceNumber; + } +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index a516527bc34..28ae7fb2f9b 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,28 @@ class INET_API OriginatorBlockAckAgreement : public cObject int bufferSize = -1; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; - bool isAddbaResponseReceived = false; + bool isCompressedBlockAckSupported = 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; + // 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) : + 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,25 +61,37 @@ 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; } + 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; } 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 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; } 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 2f11dc365db..385ca9ee757 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -7,24 +7,39 @@ #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/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.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, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) { - 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); + blockAckAgreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaRequest->getReceiverAddress())); 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; 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()); @@ -33,30 +48,104 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() return earliestTime; } -void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +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); +} + +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 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); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + 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; + 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(); + 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) { 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()); @@ -74,13 +163,21 @@ 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) { + auto agreement = getActiveAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); + if (agreement != nullptr) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } } + else if (auto compressedBlockAck = dynamicPtrCast(blockAck)) { + auto agreement = getAgreement(compressedBlockAck->getTransmitterAddress(), compressedBlockAck->getTidInfo()); + if (agreement) { + agreement->setStartingSequenceNumber(compressedBlockAck->getStartingSequenceNumber()); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } + } else throw cRuntimeError("Unsupported BlockAck"); } @@ -88,8 +185,13 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::ORIGINATOR, earliestExpirationTime); +} + +void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) +{ + callback->scheduleAddbaResponseTimer(computeEarliestAddbaResponseDeadline()); } OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid) @@ -99,6 +201,18 @@ 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)); + return it != blockAckAgreements.end() && it->second->isPending(); +} + const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode) { auto delba = makeShared(); @@ -110,66 +224,278 @@ 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::terminateAgreement(MacAddress originatorAddr, Tid tid) +{ + delete removeAgreement(originatorAddr, tid); } -void OriginatorBlockAckAgreementHandler::processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +uint64_t OriginatorBlockAckAgreementHandler::processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) { - 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); + // 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, blockAckAgreementPolicy); 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)) { - updateAgreement(agreement, addbaResp); + // 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, blockAckAgreementPolicy); + 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; } } -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(); } -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 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) { 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 = 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 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; + 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())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); + scheduleInactivityTimer(callback); + 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(); + 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) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; +} + +bool OriginatorBlockAckAgreementHandler::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 it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); + return true; } -void OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) +OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getTransmitterAddress(), delba->getTid()); + auto delba = findFragmentedActionContext(packet); + if (!delba->getInitiator()) + return {}; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + 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); + 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())); + 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); + return result; + } + } + return {}; +} + +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; + 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())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getTransmitterAddress(), delba->getTid()); + scheduleInactivityTimer(callback); + 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; } OriginatorBlockAckAgreementHandler::~OriginatorBlockAckAgreementHandler() diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4a449d17da3..33590702fe7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -21,27 +21,47 @@ 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; protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); - virtual void createAgreement(const Ptr& addbaRequest); - virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); + virtual uint8_t allocateDialogToken(); + 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); 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 void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) 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 OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) 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; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 44b4c0874fa..802a959d3f7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -17,24 +17,26 @@ 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"); aMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); - // TODO addbaFailureTimeout = par("addbaFailureTimeout"); + 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) + 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 +44,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 b762cc29828..00aae6eeb4b 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; @@ -25,7 +25,10 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi bool aMsduSupported = false; int maximumAllowedBufferSize = -1; simtime_t blockAckTimeoutValue = -1; - simtime_t addbaFailureTimeout = -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; } @@ -36,11 +39,13 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi 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; } virtual bool isDelayedAckPolicySupported() const override { return delayedAckPolicySupported; } + 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/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index bbb569b4b20..4f79b6cc3cb 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; @@ -23,6 +24,10 @@ 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 + bool localCompressedBlockAckSupported = default(false); + string compressedBlockAckPeerAddresses = default(""); @display("i=block/control"); } 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 d93711d2839..bcacac4a1a4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -12,26 +12,25 @@ namespace inet { namespace ieee80211 { -RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime) : - startingSequenceNumber(startingSequenceNumber), +RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime, uint64_t generationId) : bufferSize(bufferSize), - blockAckTimeoutValue(lastUsedTime) + blockAckTimeoutValue(lastUsedTime), + generationId(generationId) { calculateExpirationTime(); - blockAckRecord = new BlockAckRecord(originatorAddress, tid); + 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 40a4186fead..31febb4f507 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -18,26 +18,43 @@ class INET_API RecipientBlockAckAgreement : public cObject protected: BlockAckRecord *blockAckRecord = nullptr; - SequenceNumberCyclic startingSequenceNumber; 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; + // prevent that pending teardown from being re-armed by late activity. + bool inactivityExpired = false; + 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); + 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() { + 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; } virtual void addbaResposneSent() { isAddbaResponseSent = true; } - virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + 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; } 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 6219ac77140..8f9efe1d815 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -7,7 +7,10 @@ #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" namespace inet { namespace ieee80211 { @@ -17,7 +20,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; } @@ -25,8 +29,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 @@ -38,59 +42,71 @@ 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) + auto agreement = getActiveAgreement(tid, originatorAddr); + if (agreement != nullptr) { + agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); + } } } -void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +// IEEE Std 802.11-2024, 11.5.4: a one-TID BlockAckReq for an agreement's TID +// also resets the recipient inactivity timer. +void RecipientBlockAckAgreementHandler::blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) +{ + 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 (isAcceptedOneTidBlockAckReq(blockAckReq, agreement)) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } +} + +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 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); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + 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; + for (const auto& entry : blockAckAgreements) { + auto agreement = entry.second; + if (!agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) + expiredAgreements.emplace_back(entry.first, agreement->getGenerationId()); } - 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; + 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(); + 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 } - else - // TODO update? - return it->second; + scheduleInactivityTimer(agreementHandlerCallback); + return expired; } // // 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) { @@ -102,41 +118,42 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } -const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +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(); addbaResponse->setReceiverAddress(addbaRequest->getTransmitterAddress()); + // 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); 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; } -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) @@ -146,35 +163,189 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } -void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getActiveAgreement(Tid tid, MacAddress originatorAddr) { - updateAgreement(addbaResp); - scheduleInactivityTimer(callback); + auto agreement = getAgreement(tid, originatorAddr); + return agreement != nullptr && !agreement->isInactivityExpired() ? agreement : nullptr; } -void RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { 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); + 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. + 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; + } + else + blockAckAgreements[id] = agreement; + agreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaRequest->getTransmitterAddress())); + agreement->addbaResposneSent(); + agreement->setIsDelayedBlockAckPolicySupported(addbaResponse->getBlockAckPolicy() == 0); + scheduleInactivityTimer(agreementHandlerCallback); + } + procedureCallback->processMgmtFrame(addbaResponsePacket, addbaResponse); + return agreement; +} + +void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) +{ + 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) +bool RecipientBlockAckAgreementHandler::isDelbaPending(const Packet *packet, const Ptr& delba) const { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + 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(); } -void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + 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())); + scheduleInactivityTimer(callback); + pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); + return terminatedAgreement; + } + auto teardownIt = pendingTeardownGenerationIds.find(agreementId); + if (teardownIt == pendingTeardownGenerationIds.end() || teardownIt->second != agreementTag->getGenerationId()) + return nullptr; + return nullptr; + } + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + return terminatedAgreement; +} + +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; +} + +RecipientBlockAckAgreementAbortResult RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return {}; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return {}; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + 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 +{ + 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, IBlockAckAgreementHandlerCallback *callback) +{ + 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())); + scheduleInactivityTimer(callback); + return terminatedAgreement; + } + 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..b00f9cff6b1 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -26,26 +26,36 @@ 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 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 uint64_t allocateAgreementGenerationId(); 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, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processTransmittedDelba(const Ptr& delba) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + 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 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; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index 73813feaa5d..42a37c293b2 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) { + ModeSetModuleBase::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..d6abedf06fe 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/ModeSetModuleBase.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 ModeSetModuleBase, 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..5d2e931ab3d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned @@ -16,7 +16,10 @@ 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 bool delayedAckPolicySupported = default(false); bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index ab7cb050e5f..050b85759db 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -21,16 +21,27 @@ 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); + 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 47837ead38a..a3d0ab51133 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -7,45 +7,110 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" +#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" 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. [...] // -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); + ReorderBuffer framesToPassUp; + auto sequenceNumber = dataHeader->getSequenceNumber(); + auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); + 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 = 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). // 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)) { - if (dataHeader->getAckPolicy() == BLOCK_ACK) - agreement->blockAckPolicyFrameReceived(dataHeader); - 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); - return ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); - } - // 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)) }); - } + // 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) { + // 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; } - else - delete dataPacket; - return ReorderBuffer({}); + // 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) + receiveBuffer->removeFrame(SequenceNumberCyclic(entry.first)); + // Any remaining displaced entries are incomplete and cannot be delivered. + receiveBuffer->dropFramesUntil(newStartingSequenceNumber); + receiveBuffer->setNextExpectedSequenceNumber(newStartingSequenceNumber); + } + 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; + } + // 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; + } + } + result.frames = framesToPassUp; + 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; } // @@ -55,20 +120,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 id = std::make_pair(tid, blockAckReq->getTransmitterAddress()); + 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(blockAckReqDetails->tid, blockAckReq->getTransmitterAddress()); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { ReceiveBuffer *receiveBuffer = it->second; @@ -81,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 (BlockAckWindow::isBefore(receiveBuffer->getNextExpectedSequenceNumber(), startingSequenceNumber)) + receiveBuffer->setNextExpectedSequenceNumber(startingSequenceNumber); return completePrecedingMpdus; } return ReorderBuffer(); @@ -108,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 (isComplete(fragments)) - completePrecedingMpdus[sequenceNumber] = fragments; + auto currentStartingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); + for (int i = 0; i < receiveBuffer->getBufferSize(); i++) { + auto sequenceNumber = currentStartingSequenceNumber + i; + if (!BlockAckWindow::isBefore(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; } @@ -139,35 +198,26 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder auto it = buffer.find(seqNum.get()); if (it != buffer.end()) { auto fragments = it->second; - if (isComplete(fragments)) { - reorderBuffer[seqNum.get()] = fragments; + if (ReceiveBuffer::isComplete(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)); } } -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(); @@ -185,21 +235,43 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } -void BlockAckReordering::processReceivedDelba(const Ptr& delba) +simtime_t BlockAckReordering::getNextExpirationTime() const { - Tid tid = delba->getTid(); - MacAddress originatorAddr = delba->getTransmitterAddress(); + 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; 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) +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 @@ -207,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) @@ -216,7 +287,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; @@ -225,15 +296,15 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists if (earliestFragments.size() > 0) { for (auto it : buffer) { SequenceNumberCyclic currentSeqNum = it.second.at(0)->peekAtFront()->getSequenceNumber(); - if (currentSeqNum < earliestSeqNum) { - if (isComplete(it.second)) { + if (BlockAckWindow::isBefore(currentSeqNum, earliestSeqNum)) { + if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = currentSeqNum; } } } } - 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 a50606f8a53..ac3ace0a951 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -24,26 +24,41 @@ 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 + // by ascending Fragment Number. The vectors only reorder packet pointers; + // packet ownership remains with the caller of those methods. protected: std::map, ReceiveBuffer *> receiveBuffers; + simtime_t maxReceiveLifetime = SIMTIME_MAX; protected: + static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); ReorderBuffer collectCompletePrecedingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); 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); + 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); public: + struct QosFrameProcessingResult { + ReorderBuffer frames; + Fragments tombstonedFragments; + }; + + explicit BlockAckReordering(simtime_t maxReceiveLifetime = SIMTIME_MAX) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BlockAckReordering(); - void processReceivedDelba(const Ptr& delba); + 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 1dd9c959c8f..5592004173d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -7,6 +7,10 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h" +#include + +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + namespace inet { namespace ieee80211 { @@ -16,49 +20,129 @@ 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 (BlockAckWindow::isBefore(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) +bool ReceiveBuffer::canInsertFrame(const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) const { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); - // 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 false; - } - fragments.push_back(dataPacket); + if (!BlockAckWindow::isWithin(nextExpectedSequenceNumber, bufferSize, sequenceNumber)) + return false; + int retainedLength = length; + for (const auto& entry : buffer) { + if (BlockAckWindow::isBefore(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; } - else { - buffer[sequenceNumber.get()].push_back(dataPacket); + } + 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(); + bool isFragmented = dataHeader->getMoreFragments() || fragmentNumber != 0; + pruneExpiredFragmentSequences(); + if (isFragmented && expiredFragmentSequences.find(sequenceNumber.get()) != expiredFragmentSequences.end()) + return FrameInsertionResult::REJECTED_EXPIRED; + 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++; - return true; + fragments.push_back(dataPacket); + } + else { + buffer[sequenceNumber.get()].push_back(dataPacket); + bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; } - return false; + // 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) { 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; + bufferEntries.erase(it->first); it = buffer.erase(it); } else @@ -71,12 +155,68 @@ 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 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(); + 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 a499465d198..bc8c9719e8e 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,19 +47,28 @@ class INET_API ReceiveBuffer int length = 0; SequenceNumberCyclic nextExpectedSequenceNumber; + void pruneExpiredFragmentSequences(); + bool canInsertFrame(const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) const; + public: ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSequenceNumber); virtual ~ReceiveBuffer(); - bool insertFrame(Packet *dataPacket, const Ptr& dataHeader); + 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(); + 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/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 1f6ea0615cb..5b3c93abf82 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")); @@ -65,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; } @@ -122,16 +120,14 @@ void Dcaf::expectedChannelAccess(simtime_t time) // don't care } -void Dcaf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void Dcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } + 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 2fab0d312c8..2ff03b4eb25 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,12 @@ 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 { + public: + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: - physicallayer::Ieee80211ModeSet *modeSet = nullptr; IContention *contention = nullptr; IChannelAccess::ICallback *callback = nullptr; @@ -43,7 +45,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/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/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index e220635a876..fad741bbc8d 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"))); @@ -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,16 +187,14 @@ int Edcaf::getCwMin(AccessCategory ac, int aCwMin) } } -void Edcaf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void Edcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } + 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 e737cad9162..2a5f6262398 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,8 +28,11 @@ 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 { + public: + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IContention *contention = nullptr; IChannelAccess::ICallback *callback = nullptr; @@ -61,7 +64,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/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/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..462874a9611 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.cc @@ -0,0 +1,29 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#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) { + 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 new file mode 100644 index 00000000000..c0850f34b94 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetModuleBase.h @@ -0,0 +1,31 @@ +// +// 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/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, 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; + 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/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/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cc4a7b86b92..b3f4e5a97fe 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -9,16 +9,41 @@ #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 { + +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; + // 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. + 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/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/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/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index 46f9a820f28..4b02b5af006 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -42,10 +42,27 @@ 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; + + /** + * 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/IIeee80211MacConfiguration.h b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h new file mode 100644 index 00000000000..e7dcc142d6b --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IIeee80211MacConfiguration.h @@ -0,0 +1,24 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef INET_IIEEE80211MACCONFIGURATION_H +#define INET_IIEEE80211MACCONFIGURATION_H + +#include "inet/linklayer/ieee80211/mac/contract/IIeee80211ModeSetProvider.h" + +namespace inet::ieee80211 { +/** MAC configuration extends catalog access with preparation after PHY readiness. + * Repeated preparation preserves protocol and algorithm state. + */ +class INET_API IIeee80211MacConfiguration : public IIeee80211ModeSetProvider +{ + public: + virtual ~IIeee80211MacConfiguration() = default; + 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..4bdadf22c67 --- /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 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/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/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..b6f3ba38e0f 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,53 @@ 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; +}; + +struct INET_API OriginatorBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + 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 blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 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 OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 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; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index 40ea82aa95e..1c0293f4c0a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -26,9 +26,11 @@ 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 bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) 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/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 8f03c2fd052..0fac80fddff 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,10 @@ 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 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/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 23796ec3556..062d81b4736 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; @@ -33,11 +38,24 @@ class INET_API IReassembly virtual Packet *addFragment(Packet *frame) = 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. + * 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 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/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 357745c3ddb..d10380b03e3 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" @@ -17,21 +19,41 @@ #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { + +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: 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, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; + virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 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; + // 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; }; } // namespace ieee80211 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/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/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 8a3ff4c0b7c..6a0696a2fb3 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: + 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; + }; + 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/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 ddb1cf9aa65..65bfef59d83 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -7,11 +7,14 @@ #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" #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" @@ -25,7 +28,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")); @@ -53,7 +56,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); } } @@ -80,6 +83,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()); } @@ -121,19 +130,32 @@ 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) { + 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"); + 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); } } - else - ModeSetListener::receiveSignal(source, signalID, obj, details); +} + +void Dcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); } void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) @@ -147,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"); @@ -161,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; @@ -170,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); @@ -214,6 +372,7 @@ void Dcf::frameSequenceFinished() if (hasFrameToTransmit()) channelAccess->requestChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review + cancelledManagementTransactions.clear(); } bool Dcf::isReceptionInProgress() @@ -276,12 +435,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"; @@ -290,7 +461,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); @@ -301,8 +472,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; @@ -356,6 +531,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); @@ -369,6 +553,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"; @@ -377,7 +564,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); @@ -429,4 +616,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 51ce7172954..b77da154fc3 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -8,8 +8,10 @@ #ifndef __INET_DCF_H #define __INET_DCF_H +#include + #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" @@ -28,6 +30,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 { @@ -37,7 +40,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; @@ -79,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; @@ -87,7 +101,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(); @@ -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; @@ -118,12 +136,16 @@ 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; 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/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..17b61a887ab 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -7,13 +7,22 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + +#include + #include "inet/common/ModuleAccess.h" #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/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" +#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" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" @@ -28,16 +37,18 @@ 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); 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"); 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"))); @@ -63,14 +74,239 @@ void Hcf::initialize(int stage) originatorBlockAckAgreementHandler = new OriginatorBlockAckAgreementHandler(); originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); + originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { + if (auto addbaReq = findFragmentedActionContext(packet)) + return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); + 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 + // 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(packetDroppedSignal, 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 = findFragmentedActionContext(packet); + 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 == 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 = 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; + } + 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) { + 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); + } + shouldResume |= processDroppedBlockAckSetupFrame(packet); + shouldResume |= processDroppedBlockAckTeardownFrame(packet); } + if (shouldResume) + 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 @@ -101,8 +337,23 @@ 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"); + } + else if (msg == addbaResponseTimer) { + if (originatorBlockAckAgreementHandler) { + originatorBlockAckAgreementHandler->addbaResponseTimeoutExpired(originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); } else throw cRuntimeError("Unknown event"); @@ -113,7 +364,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()); @@ -144,8 +395,9 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); + trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (!pendingQueue->isEmpty()) { + if (!blockAckInactivityExpiryInProgress && hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -154,19 +406,49 @@ 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 (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; } - else - ModeSetListener::receiveSignal(source, signalID, obj, details); + 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) @@ -175,10 +457,151 @@ 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) +{ + Enter_Method("scheduleAddbaResponseTimer"); + if (deadline == SIMTIME_MAX) { + if (addbaResponseTimer->isScheduled()) + cancelEvent(addbaResponseTimer); + } + else + 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"); + // 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::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) @@ -190,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; @@ -225,13 +655,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 @@ -251,12 +690,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; @@ -277,14 +722,19 @@ void 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); edcaf->getAckHandler()->dropFrame(internallyCollidedHeader); PacketDropDetails details; details.setReason(RETRY_LIMIT_REACHED); 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); @@ -295,6 +745,7 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else edcaf->requestChannel(this); } + return handledCollisions; } /* @@ -309,12 +760,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); @@ -323,6 +773,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) @@ -337,8 +788,10 @@ 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); + if (receptionResult.completeHeader != nullptr) + recipientProcessReceivedManagementFrame(receptionResult.completeHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -351,9 +804,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(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 @@ -361,29 +816,82 @@ 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); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } } else if (auto addbaResp = dynamicPtrCast(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()->setGenerationId(response.teardownTransactionId); + processMgmtFrame(delbaPacket, response.teardownDelba); + } + resumeEligibleChannelAccess(); } else if (auto delba = dynamicPtrCast(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 pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy, this); + 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 { - 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) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } + resumeEligibleChannelAccess(); } } else @@ -409,6 +917,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) { @@ -424,24 +945,38 @@ 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 = findFragmentedActionContext(packet); + 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); + auto transactionTag = packet->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); 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 (notifyManagement) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } } @@ -452,11 +987,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(); @@ -466,7 +1005,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); } @@ -480,28 +1019,50 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this); + 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); } -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 (auto addbaReq = findFragmentedActionContext(packet)) { 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); - else - recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + else if (findFragmentedActionContext(packet)) + ; // Recipient agreement was established when the successful response was formed. + else if (auto delba = findFragmentedActionContext(packet)) { + if (delba->getInitiator()) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); + if (wasPending) + rebuildPendingFrameEligibility(); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } + } + else { + 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 + // 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(); + } + } } else ; // TODO other mgmt frames if needed } @@ -520,6 +1081,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) { @@ -551,23 +1125,37 @@ 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 = findFragmentedActionContext(failedPacket); + 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); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); 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 (notifyManagement) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + } } } else { @@ -633,6 +1221,11 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); + if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { + bool acknowledged = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this); + if (acknowledged) + rebuildPendingFrameEligibility(); + } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { @@ -640,13 +1233,41 @@ 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) { + cancelBlockAckTeardown(true, dataHeader->getReceiverAddress(), dataHeader->getTid(), obsoleteTeardownTransactionId, nullptr); + rebuildPendingFrameEligibility(); + } + if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) + rebuildPendingFrameEligibility(); + } + } } - else if (auto blockAck = dynamicPtrCast(header)) { - EV_INFO << "BasicBlockAck has arrived" << std::endl; + else if (auto blockAck = dynamicPtrCast(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)) + 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(); 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; @@ -659,7 +1280,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"); @@ -674,7 +1295,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"); } @@ -683,11 +1304,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) @@ -704,7 +1340,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); @@ -740,7 +1376,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); @@ -814,8 +1450,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; @@ -828,4 +1467,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 72f7af70fc4..4faa36a39d8 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,9 +8,14 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#include +#include +#include +#include + #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" @@ -29,13 +34,13 @@ #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" #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 { @@ -45,12 +50,13 @@ 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; static simsignal_t blockAckAgreementAddedSignal; static simsignal_t blockAckAgreementDeletedSignal; + static simsignal_t blockAckAgreementChangedSignal; protected: Ieee80211Mac *mac = nullptr; @@ -58,6 +64,13 @@ 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; + bool blockAckInactivityExpiryInProgress = false; // Transmission and Reception IRx *rx = nullptr; @@ -96,9 +109,28 @@ 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; + // 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; + std::set> blockAckTeardownsBeingCancelled; + // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -107,26 +139,38 @@ 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); - 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 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); + virtual void untrackPendingFrame(const Packet *packet); + virtual void rebuildPendingFrameEligibility(); virtual bool isReceptionInProgress(); // 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 - 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); @@ -158,13 +202,19 @@ 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 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; 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/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 31b8bfc7047..cd8113cc9b0 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; @@ -54,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); @@ -79,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/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index e47ce922aab..9521f5f9c5f 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -29,23 +29,44 @@ 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)) { + 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; } + 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..41a01e36158 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -17,45 +17,231 @@ 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; - // FIXME temporary fix for mgmt frames - if (dynamicPtrCast(header)) - return packet; - // find entry for this frame - Key key; - key.macAddress = header->getTransmitterAddress(); - 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); - auto& value = fragmentsMap[key]; + 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); + + 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); + } + 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(); + it = fragmentsMap.emplace(key, value).first; + } + auto& value = it->second; value.fragments.resize(16); // 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); @@ -65,35 +251,82 @@ 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; + } } -void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +simtime_t BasicReassembly::getNextExpirationTime() const { - 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();) { + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& entry : fragmentsMap) + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} + +std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto& entry = *it; + auto& value = entry.second; + if (currentTime >= value.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : value.fragments) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + 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; +} + +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 : + sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; + }; + 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; + if (fragment != nullptr) + purgedFragments.push_back(fragment); it = fragmentsMap.erase(it); } - itStart = fragmentsMap.begin(); + else + it++; } - for (auto it = itStart; it != itEnd;) { - for (auto fragment : it->second.fragments) - delete fragment; - it = fragmentsMap.erase(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; } + 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 0c1d7d810cc..e63cb2325a0 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,7 +8,12 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include +#include +#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" @@ -18,25 +23,78 @@ 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; - 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))); } + 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(); } }; 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 + int terminalFragmentNumber = -1; + bool hasContradictoryTerminalFragmentNumbers = false; + simtime_t receptionStartTime; }; 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) {} 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; }; } // 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..2f5897ebf0c 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,22 @@ 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); + 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 management body out of INET's combined + // typed header before slicing it into fragment bodies. + 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()))); + } + 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 +75,17 @@ 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 (managementHeader != nullptr) + fragmentHeader = copyManagementHeader(managementHeader); + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); + if (actionFrame != nullptr) { + auto actionContext = staticPtrCast(actionFrame->dupShared()); + actionContext->setFragmentNumber(i); + actionContext->setMoreFragments(!lastFragment); + fragment->addTag()->setActionFrame(actionContext); + } 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/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/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index 1eddb2b2048..20d509f09a2 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,19 +46,28 @@ void FrameSequenceHandler::handleStartRxTimeout() } } -void FrameSequenceHandler::processResponse(Packet *frame) +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; + } switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { - // 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"); @@ -52,7 +79,10 @@ void FrameSequenceHandler::processResponse(Packet *frame) void FrameSequenceHandler::transmissionComplete() { if (isSequenceRunning()) { - finishFrameSequenceStep(); + if (frameSequenceCancellationRequested) + abortFrameSequence(); + else + finishFrameSequenceStep(); if (isSequenceRunning()) startFrameSequenceStep(); } @@ -63,6 +93,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 +106,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 +154,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 +180,7 @@ void FrameSequenceHandler::finishFrameSequence() context = nullptr; frameSequence = nullptr; callback = nullptr; + frameSequenceCancellationRequested = false; inProgressFrames->clearDroppedFrames(); } @@ -166,6 +204,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..21a4eec7be1 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -21,21 +21,23 @@ 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; } 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; } + virtual void cancelFrameSequence() override { if (isSequenceRunning()) frameSequenceCancellationRequested = true; } + virtual void abortFrameSequence() override; virtual ~FrameSequenceHandler(); }; 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/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/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 9ad68ed2969..d62158e4e38 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -7,9 +7,27 @@ #include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" + 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) @@ -77,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; @@ -149,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; @@ -221,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; @@ -267,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; @@ -311,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; @@ -355,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; @@ -392,12 +410,18 @@ 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); - 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); } @@ -405,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; @@ -425,7 +449,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/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/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/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/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/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 90cc7602199..db8475cd56c 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -7,8 +7,12 @@ #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" + namespace inet { namespace ieee80211 { @@ -16,7 +20,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"); @@ -31,13 +35,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 @@ -52,22 +63,47 @@ 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; + // 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) +{ + // 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. + // The agreement snapshots peer capability state when it is established. + if (agreement == nullptr || !agreement->getIsCompressedBlockAckSupported() || !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; } // 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 +111,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 d25da018cdc..e6209e02560 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; @@ -31,16 +31,16 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat 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; - + 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; 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 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 eb2f9236ab1..d4e0dceb93d 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; @@ -26,4 +27,3 @@ simple OriginatorQosAckPolicy extends SimpleModule like IOriginatorQosAckPolicy @display("i=block/control"); } - 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/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 59bdd8b4372..b28fa8d8f0c 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 { @@ -178,6 +180,21 @@ void QosAckHandler::processFailedBlockAckReq(const Ptr& header) { if (header->getType() == ST_DATA_WITH_QOS) { @@ -198,20 +215,23 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const Ptr& blockAckReq) { for (auto& ackStatus : ackStatuses) { + 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) + // 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; } } 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)? + // 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; } } @@ -276,6 +296,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/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..a85e98c3680 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc @@ -20,9 +20,10 @@ Define_Module(TxopProcedure); void TxopProcedure::initialize(int stage) { - ModeSetListener::initialize(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 656b74e8055..c37a58a6e83 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; @@ -33,6 +33,7 @@ class INET_API TxopProcedure : public ModeSetListener 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/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..7485427ca68 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"))); } @@ -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/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/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/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..2ae0237638a 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,16 +62,13 @@ void RateControlBase::emitDatarateChangedSignal(const MacAddress& receiver, cons } } -void RateControlBase::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void RateControlBase::applyModeSet(const Ieee80211ModeSet *newModeSet) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - resetRateControl(); - } + 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 2a61b5028fa..d4072b5fe81 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -9,21 +9,21 @@ #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: + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; static simsignal_t datarateChangedSignal; 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..47c7afdd833 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" @@ -14,6 +15,30 @@ 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); + bool hasMandatoryLegacyMode = false; + const IIeee80211Mode *legacyMode = nullptr; + for (const auto *candidate : modeSet->getLegacyOperationalModes()) { + if (!modeSet->getIsMandatory(candidate)) + continue; + hasMandatoryLegacyMode = true; + if (requestedMode != nullptr && 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; +} + namespace { static const IIeee80211Mode *getLegacyFallback(const Ieee80211ModeSet *modeSet, @@ -49,12 +74,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 +90,12 @@ 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, 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 @@ -105,28 +135,36 @@ 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; if (modeSet == nullptr) throw cRuntimeError("Cannot select a peer-compatible HT mode without an IEEE 802.11 mode set"); - if (!modeSet->containsMode(mode)) - throw cRuntimeError("HT mode '%s' is not contained in IEEE 802.11 mode set '%s'", + if (!modeSet->supportsMode(mode)) + throw cRuntimeError("HT mode '%s' is not supported by IEEE 802.11 mode set '%s'", mode->getName(), modeSet->getName()); - if (peerHtState == nullptr || !peerHtState->valid || !peerHtState->negotiatedCapabilities.localTxPeerRx.valid) + if (!htEligible || operation == nullptr || peerHtState == nullptr || !peerHtState->valid || + !peerHtState->negotiatedCapabilities || !peerHtState->negotiatedCapabilities->localTxPeerRx.valid) return getLegacyFallback(modeSet, mode, peerAddress); - if (isCompatibleHtMode(mode, peerHtState)) - return mode; + + const IIeee80211Mode *effectiveMode = mode; + if (effectiveMode->isHtGreenfield() && !peerHtState->negotiatedCapabilities->localTxPeerRx.receiverGreenfield) + effectiveMode = modeSet->findHtMixedMode(mode); + if (effectiveMode != nullptr && isCompatibleHtMode(effectiveMode, peerHtState, *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)) + !isCompatibleHtMode(candidate, peerHtState, *operation)) continue; if (bestMode == nullptr || isBetterHtMode(candidate, bestMode)) bestMode = candidate; @@ -134,5 +172,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 ba5eaebf764..ce2c8974768 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -14,6 +14,14 @@ 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); + /** * 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 @@ -24,7 +32,15 @@ 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); + +// 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 add5ec700f1..20d95b3e58d 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -18,29 +18,35 @@ namespace ieee80211 { using namespace inet::physicallayer; +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), 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; + } + 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) { - ModeSetListener::initialize(stage); + ModeSetModuleBase::initialize(stage); if (stage == INITSTAGE_LOCAL) mib.reference(this, "mibModule", true); if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); - double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); - double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); - 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)); + if (modeSet == nullptr) + throw cRuntimeError("QosRateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); + updateModes(); } } @@ -56,7 +62,7 @@ void QosRateSelection::ensurePerReceiverModesResolved() throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); auto networkInterface = check_and_cast(module); try { - auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; } catch (const cRuntimeError& e) { @@ -65,6 +71,39 @@ void QosRateSelection::ensurePerReceiverModesResolved() } } +void QosRateSelection::updateModes() +{ + if (modeSet == nullptr) + return; + double multicastFrameBitrate = par("multicastFrameBitrate"); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + double dataFrameBitrate = par("dataFrameBitrate"); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + double mgmtFrameBitrate = par("mgmtFrameBitrate"); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + 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), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); + double responseBlockAckFrameBitrate = par("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, Hz(par("responseCtsFrameBandwidth")), par("responseCtsFrameNumSpatialStreams"), par("responseCtsFrameGuardInterval"), 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; +} + const IIeee80211Mode *QosRateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -96,40 +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 (modeSet->getIsMandatory(mode)) - responseMode = mode; - else if (auto slowerMode = modeSet->getSlowerMandatoryMode(mode)) - responseMode = slowerMode; - 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 (modeSet->getIsMandatory(mode)) - responseMode = mode; - else if (auto slowerMode = modeSet->getSlowerMandatoryMode(mode)) - responseMode = slowerMode; - 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; } // @@ -140,19 +166,33 @@ 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) && !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); + } + auto peerAddress = mib && blockAckReq->getReceiverAddress() == mib->address ? blockAckReq->getTransmitterAddress() : blockAckReq->getReceiverAddress(); + return getPeerCompatibleMode(peerAddress, responseMode); } const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { + if (dataOrMgmtHeader->getReceiverAddress().isMulticast()) { + const auto *requestedMode = multicastFrameMode; + if (requestedMode == nullptr) + requestedMode = dynamicPtrCast(dataOrMgmtHeader) ? dataFrameMode : mgmtFrameMode; + return selectGroupAddressedMode(modeSet, requestedMode != nullptr ? requestedMode : fastestMandatoryMode); + } // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). // Wins over the interface-wide dataFrameMode / rate control; group-addressed and management - // frames are left to the existing rules below. + // frames were handled above. if (dynamicPtrCast(dataOrMgmtHeader) && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) { ensurePerReceiverModesResolved(); auto it = perReceiverDataFrameMode.find(dataOrMgmtHeader->getReceiverAddress()); @@ -163,46 +203,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) @@ -210,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 @@ -242,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; @@ -270,14 +274,13 @@ const IIeee80211Mode *QosRateSelection::computeMode(Packet *packet, const PtrgetReceiverAddress(), computeControlFrameMode(header, txopProcedure)); } -void QosRateSelection::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void QosRateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); - } + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); } void QosRateSelection::frameTransmitted(Packet *packet, const Ptr& header) @@ -288,11 +291,16 @@ 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->findPeerHtState(peerAddress), mode, peerAddress); + return selectPeerCompatibleMode(modeSet, mib->findPeerCapabilities(peerAddress), mode, peerAddress, + mib->hasHtOperation() ? &mib->getHtOperation() : nullptr, mib->relationshipAllowsHt(peerAddress)); } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 569ef742d37..2d315f36b65 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,15 @@ 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 { + public: + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; - const physicallayer::Ieee80211ModeSet *modeSet = nullptr; std::map lastTransmittedFrameMode; // originator frame modes @@ -58,8 +60,7 @@ 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; - + 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. @@ -92,4 +93,3 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 35bf0e1ace5..2358c211da8 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -21,19 +21,44 @@ 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"); 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); // Unspecified by default - int dataFrameNumSpatialStreams = default(-1); // Unspecified by default + double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified + int dataFrameNumSpatialStreams = default(-1); // Explicit mode qualifier when dataFrameBitrate is specified + double dataFrameGuardInterval @unit(s) = default(-1s); // Explicit mode qualifier when dataFrameBitrate is specified; negative means unspecified, otherwise it must equal the selected PHY mode's modeled GI // Per-receiver unicast data-frame rate. Keys are peer interface module paths (e.g. // "host1.wlan[0]"), resolved to MAC addresses at run time; values are bitrates (bps). diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 9d28a5142cd..15c4bf2d2db 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" @@ -22,29 +23,36 @@ namespace ieee80211 { using namespace inet::physicallayer; +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), 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; + } + 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) { + 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"))); - 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")); - 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(); + if (modeSet == nullptr) + throw cRuntimeError("RateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); + updateModes(); // WATCH(dataOrMgmtRateControl); // WATCH(*((cObject**)&fastestMandatoryMode)); @@ -75,7 +83,7 @@ void RateSelection::ensurePerReceiverModesResolved() throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); auto networkInterface = check_and_cast(module); try { - auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; } catch (const cRuntimeError& e) { @@ -84,6 +92,38 @@ void RateSelection::ensurePerReceiverModesResolved() } } +void RateSelection::updateModes() +{ + if (modeSet == nullptr) + return; + double multicastFrameBitrate = par("multicastFrameBitrate"); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + double dataFrameBitrate = par("dataFrameBitrate"); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + double mgmtFrameBitrate = par("mgmtFrameBitrate"); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + 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), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); + double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); + 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 + // 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; +} + const IIeee80211Mode *RateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -103,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->getIsMandatory(mode) ? mode : modeSet->getSlowerMandatoryMode(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->getIsMandatory(mode) ? mode : modeSet->getSlowerMandatoryMode(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. @@ -140,28 +184,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. @@ -184,14 +228,13 @@ const IIeee80211Mode *RateSelection::computeMode(Packet *packet, const Ptr(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); - } + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); } void RateSelection::frameTransmitted(Packet *packet, const Ptr& header) @@ -221,11 +264,16 @@ void RateSelection::emitDatarateSelected(cComponent *emitter, const PtrgetHtMcsIndex() < 0) + if (mode == nullptr || peerAddress.isMulticast() || !mib) return mode; - return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); + 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)); } } // 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 69441331453..720ff8a76fb 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -9,11 +9,12 @@ #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" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { namespace ieee80211 { @@ -31,14 +32,17 @@ 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 { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; const physicallayer::IIeee80211Mode *fastestMandatoryMode = nullptr; - const physicallayer::Ieee80211ModeSet *modeSet = nullptr; std::map lastTransmittedFrameMode; // originator frame modes @@ -58,8 +62,7 @@ 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; - + 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. @@ -100,4 +103,3 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 89f63916421..150a480ce89 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -17,18 +17,37 @@ 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"); 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); // 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/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/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index 5075d391e89..65fd471fcc5 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -18,13 +18,46 @@ 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(); + basicReassembly = new BasicReassembly(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 +105,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..1b838483d49 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); // 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/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 29f793f0110..647defe934c 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -8,6 +8,8 @@ #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 { namespace ieee80211 { @@ -16,15 +18,15 @@ Define_Module(RecipientQosAckPolicy); void RecipientQosAckPolicy::initialize(int stage) { - ModeSetListener::initialize(stage); - if (stage == INITSTAGE_LOCAL) { + ModeSetModuleBase::initialize(stage); + if (stage == INITSTAGE_LOCAL) rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); - } } -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 @@ -62,14 +64,23 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { if (dynamicPtrCast(blockAckReq)) { - return agreement != nullptr; + 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. } + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) + return isCompressedBlockAckNeeded(compressedBlockAckReq, agreement); else throw cRuntimeError("Unsupported BlockAckReq"); } +bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) +{ + // 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. + return isAcceptedOneTidBlockAckReq(blockAckReq, agreement); +} + // // 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 @@ -89,9 +100,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 9f4ce456784..d47b88d6fba 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; @@ -25,7 +25,8 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient 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); + simtime_t computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const; simtime_t computeAckDuration(Packet *packet, const Ptr& dataOrMgmtHeader) const; public: @@ -33,11 +34,10 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient 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 50971923a80..89eea501c93 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned @@ -16,8 +16,8 @@ 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/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 77e677bb147..85394b563db 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" @@ -23,28 +24,97 @@ 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(); + basicReassembly = new BasicReassembly(maxReceiveLifetime); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); - blockAckReordering = new BlockAckReordering(); + blockAckReordering = new BlockAckReordering(maxReceiveLifetime); + receiveLifetimeTimer = new cMessage("receiveLifetimeTimer"); +} + +void RecipientQosMacDataService::handleMessage(cMessage *message) +{ + if (message != receiveLifetimeTimer) + throw cRuntimeError("Unknown message"); + expireReceiveLifetime(); + scheduleReceiveLifetimeTimer(); +} + +void RecipientQosMacDataService::expireReceiveLifetime() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + 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::scheduleReceiveLifetimeTimer() +{ + if (receiveLifetimeTimer->isScheduled()) + cancelEvent(receiveLifetimeTimer); + auto nextExpirationTime = std::min(basicReassembly->getNextExpirationTime(), blockAckReordering->getNextExpirationTime()); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, receiveLifetimeTimer); +} + +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; + } + } + 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) { + expireReceiveLifetime(); + 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; + scheduleReceiveLifetimeTimer(); + if (defragmentedPacket != nullptr) + emit(packetDefragmentedSignal, defragmentedPacket); + return defragmentedPacket; } Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { + expireReceiveLifetime(); auto packet = basicReassembly->addFragment(mgmtFragment); + scheduleReceiveLifetimeTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -57,23 +127,48 @@ 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 + 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; details.setReason(DUPLICATE_DETECTED); emit(packetDroppedSignal, dataPacket, &details); delete dataPacket; + scheduleReceiveLifetimeTimer(); 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(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + if (agreement == nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); + 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 @@ -93,6 +188,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data else ; // TODO drop? } } + scheduleReceiveLifetimeTimer(); std::vector deaggregatedFrames; if (aMsduDeaggregation) { for (auto defragmentedFrame : defragmentedFrames) { @@ -112,46 +208,67 @@ 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); + expireReceiveLifetime(); // 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; + // 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()) { + scheduleReceiveLifetimeTimer(); + return { {}, mgmtHeader, true }; + } + else { + scheduleReceiveLifetimeTimer(); + return { {}, nullptr, true }; + } + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); + 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 std::vector(); + scheduleReceiveLifetimeTimer(); + return { {}, completeHeader, false }; + } + else { + scheduleReceiveLifetimeTimer(); + return { { mgmtPacket }, completeHeader, false }; } - else - return std::vector({ mgmtPacket }); } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + 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 (blockAckReqDetails) { BlockAckReordering::ReorderBuffer frames; - if (blockAckReordering) { - Tid tid = blockAckReq->getTidInfo(); - MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) - frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); - else - return std::vector(); - } + if (blockAckReordering) + frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReqDetails->blockAckReq); std::vector defragmentedFrames; 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 { @@ -180,13 +297,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(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 e4b8ee28865..b2c7a8a03b3 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,6 +27,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; + cMessage *receiveLifetimeTimer = nullptr; + simtime_t maxReceiveLifetime = SIMTIME_MAX; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -41,14 +43,18 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, protected: virtual ~RecipientQosMacDataService(); virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReceiveLifetime(); + virtual void scheduleReceiveLifetimeTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); 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/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index 307fb523210..f6fd0bf7316 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); // 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/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..628d635b8f3 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" @@ -63,6 +64,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); } } @@ -90,8 +92,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 +131,39 @@ 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->removePeerCapabilities(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); + 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 // 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); @@ -200,6 +204,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(); @@ -207,6 +214,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"; @@ -216,9 +234,11 @@ void Ieee80211MgmtAp::sendBeacon() body->setBeaconInterval(beaconInterval); body->setChannelNumber(getDsssParameterSetChannel()); addHtCapabilities(body); - if (mib->isHtOperationSupported()) + 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); } @@ -234,7 +254,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 @@ -249,12 +269,12 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + supersedePendingAssociation(sta); + bool wasAssociated = mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); - mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; - mib->removePeerHtCapabilities(sta->address); + mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::NOT_AUTHENTICATED); + mib->removePeerCapabilities(sta->address); sta->authSeqExpected = 1; if (wasAssociated) sendDisAssocNotification(sta->address); @@ -289,11 +309,11 @@ 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->removePeerHtCapabilities(sta->address); + mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::AUTHENTICATED); // TODO only when ACK of this frame arrives + mib->removePeerCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); EV << "STA authenticated\n"; @@ -312,14 +332,14 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + supersedePendingAssociation(sta); + 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); + mib->removePeerCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); } @@ -340,7 +360,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 +370,7 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrpeekData(); - bool pendingHtOperationValid = mib->isHtOperationSupported(); + bool pendingHtOperationValid = mib->isLocalHtCapable(); Ieee80211HtOperation pendingHtOperation; if (pendingHtOperationValid) pendingHtOperation = mib->getHtOperation(); @@ -368,6 +388,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; @@ -382,13 +409,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; @@ -397,7 +427,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); @@ -420,7 +452,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 +462,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(); @@ -448,6 +480,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; @@ -461,13 +500,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; @@ -476,7 +518,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); @@ -493,12 +537,12 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + supersedePendingAssociation(sta); + bool wasAssociated = mib->getPeerAssociationStatus(sta->address) == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); - mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::AUTHENTICATED; - mib->removePeerHtCapabilities(sta->address); + mib->setPeerAssociationStatus(sta->address, Ieee80211Mib::AUTHENTICATED); + mib->removePeerCapabilities(sta->address); if (wasAssociated) sendDisAssocNotification(sta->address); } @@ -530,9 +574,11 @@ void Ieee80211MgmtAp::handleProbeRequestFrame(Packet *packet, const PtrsetBeaconInterval(beaconInterval); body->setChannelNumber(getDsssParameterSetChannel()); addHtCapabilities(body); - if (mib->isHtOperationSupported()) + 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); } @@ -566,6 +612,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..bf1f249bff2 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 { @@ -37,6 +38,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; @@ -73,6 +77,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 +106,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/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..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 { @@ -34,20 +37,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 +98,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 +137,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..8023005a4c0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -9,12 +9,14 @@ #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" #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" @@ -34,55 +36,122 @@ void Ieee80211MgmtBase::initialize(int stage) myIface = getContainingNicModule(this); numMgmtFramesReceived = 0; numMgmtFramesDropped = 0; - getContainingNicModule(this)->subscribe(modesetChangedSignal, this); + configurationProvider.reference(this, "macModule", true); + if (auto coordinator = dynamic_cast(configurationProvider.get())) + coordinator->registerModeSetConsumer(this, IIeee80211ModeSetCoordinator::MANAGEMENT_STATE); WATCH(numMgmtFramesReceived); WATCH(numMgmtFramesDropped); } + else if (stage == INITSTAGE_LINK_LAYER) { + prepareConfiguration(); + if (isUp()) + prepareLocalOperation(); + mib->publishStateChange(); + } +} + +void Ieee80211MgmtBase::prepareConfiguration() +{ + if (configurationPrepared) + return; + configurationProvider->prepareLocalCapabilities(); + modeSet = configurationProvider->getConfiguredModeSet(); + 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::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) -{ - 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()); +void Ieee80211MgmtBase::updateSupportedRates() +{ + 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::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->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 +180,7 @@ void Ieee80211MgmtBase::handleMessageWhenUp(cMessage *msg) } else throw cRuntimeError("Unknown message"); + mib->publishStateChange(); } void Ieee80211MgmtBase::sendDown(Packet *frame) @@ -187,14 +257,16 @@ void Ieee80211MgmtBase::processFrame(Packet *packet, const PtrclearPeerHtCapabilities(); + mib->clearBss(); + mib->publishStateChange(); } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index 145da89e902..73d9b5f5486 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -14,11 +14,13 @@ #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" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { @@ -28,14 +30,20 @@ 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; 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 +54,8 @@ 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(); + void updateSupportedRates(); /** Dispatches incoming messages to handleTimer(), handleUpperMessage() or processFrame(). */ virtual void handleMessageWhenUp(cMessage *msg) override; @@ -82,7 +91,14 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener 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(); + virtual void addHtCapabilities(const Ptr& frame) const; virtual void addHtOperation(const Ptr& frame, const physicallayer::IIeee80211Band *band) const; @@ -109,7 +125,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(); } @@ -124,4 +140,3 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg index 4725c5e1110..1780543eb71 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; @@ -180,6 +181,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 +210,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 5fb46ae01ad..ff3d63145d9 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 @@ -312,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"); @@ -328,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()); @@ -336,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) @@ -425,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)) { @@ -501,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); } @@ -611,18 +765,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; } @@ -643,30 +795,26 @@ 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; // 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); + 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. @@ -680,16 +828,11 @@ 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); + 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. @@ -704,15 +847,11 @@ 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); + 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. @@ -730,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. @@ -745,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. @@ -760,15 +899,11 @@ 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); + 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. @@ -799,15 +934,11 @@ 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); + 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. @@ -841,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)) { @@ -864,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)) { @@ -877,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)) { @@ -892,31 +1023,31 @@ 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)) { 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); 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)) { 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); 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)) { @@ -935,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)) { @@ -954,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 859a5c108ea..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" @@ -152,8 +153,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 +353,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()); @@ -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; @@ -378,7 +381,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()); @@ -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); @@ -416,7 +421,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 @@ -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); } @@ -532,7 +538,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 +576,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 +598,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 +619,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; - mib->removePeerHtCapabilities(assocAP.address); + ASSERT(mib->getBssStationData().isAssociated); + mib->setAssociated(false); + mib->removePeerCapabilities(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 +673,7 @@ void Ieee80211MgmtSta::stop() clearAPList(); - if (mib->bssStationData.isAssociated) + if (mib->getBssStationData().isAssociated) clearCurrentAssociation(); else { cancelAndDelete(assocAP.beaconTimeoutMsg); @@ -793,7 +801,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 @@ -825,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); @@ -850,7 +858,7 @@ void Ieee80211MgmtSta::handleDeauthenticationFrame(Packet *packet, const PtrisAuthenticated = false; - mib->removePeerHtCapabilities(address); + mib->removePeerCapabilities(address); delete packet; } @@ -896,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; @@ -922,30 +939,33 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrbssStationData.isAssociated || assocAP.address != ap->address) - mib->removePeerHtCapabilities(ap->address); + else if (!mib->getBssStationData().isAssociated || assocAP.address != ap->address) + mib->removePeerCapabilities(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->removePeerHtCapabilities(assocAP.address); + mib->setAssociated(false); + mib->removePeerCapabilities(assocAP.address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; assocAP = AssociatedApInfo(); } // change our state to "associated" - mib->bssData.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); + mib->removePeerCapabilities(ap->address); if (responseHtStatus == HtAssociationResponseStatus::INVALID_HT) { EV_WARN << "Association succeeded without usable HT negotiation with AP address=" << ap->address << ": " << responseHtReason << "\n"; @@ -958,10 +978,13 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrsetPeerVhtCapabilities(ap->address, responseVhtCapabilities, responseVhtOperation); + else + mib->removePeerVhtCapabilities(ap->address); - assocAP.beaconTimeoutMsg = new cMessage("beaconTimeout", MK_BEACON_TIMEOUT); - scheduleAfter(MAX_BEACONS_MISSED * assocAP.beaconInterval, assocAP.beaconTimeoutMsg); + mib->publishStateChange(); + emit(l2AssociatedSignal, myIface, ap); } // report back to agent @@ -983,7 +1006,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 +1039,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 +1050,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 +1058,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 +1081,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) + mib->removePeerCapabilities(ap->address); + if (mib->getBssStationData().isAssociated) changeChannel(assocAP.channel); } } @@ -1107,7 +1130,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 +1139,7 @@ void Ieee80211MgmtSta::handleBeaconFrame(Packet *packet, const PtrpublishStateChange(); delete packet; } @@ -1154,10 +1178,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; @@ -1220,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; @@ -1235,6 +1265,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; @@ -1242,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; @@ -1249,28 +1283,26 @@ 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"; - mib->removePeerHtCapabilities(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 326c7638644..39090accd92 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; @@ -79,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; @@ -125,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/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/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/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..fad22e8e288 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 @@ -72,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; @@ -83,16 +100,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); @@ -116,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/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index e5c8fd008a3..43accb52a97 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" @@ -18,16 +17,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,186 +46,258 @@ 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 |= !peerVhtStates.empty(); + peerVhtStates.clear(); + 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) -{ - // 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(); +void Ieee80211Mib::installLocalHtCapabilities(const Ieee80211HtCapabilities& capabilities, bool htSupported) +{ + 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); + 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::findPeerCapabilities(const MacAddress& address) const +{ + auto it = peerHtStates.find(address); + 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 = negotiateHtCapabilities(localHtCapabilities, - entry.second.advertisedCapabilities, htOperation); - if (++entry.second.generation == 0) - entry.second.generation = 1; - } + 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); + 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 { - auto it = peerHtStates.find(address); - return it == peerHtStates.end() || !it->second.valid ? nullptr : &it->second; + return relationshipAllowsHt(address) ? findPeerCapabilities(address) : nullptr; } -void Ieee80211Mib::setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities, - const Ieee80211HtOperation& operation) +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(); } +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) @@ -252,6 +326,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 +353,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 +367,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,17 +386,20 @@ short Ieee80211Mib::allocateAssociationId(const MacAddress& address) void Ieee80211Mib::releaseAssociationId(const MacAddress& address) { + checkStateMutation(); associationIdReservations.erase(address); - bssAccessPointData.associationIds.erase(address); - removePeerHtCapabilities(address); + stateChangePending |= bssAccessPointData.associationIds.erase(address) != 0; + removePeerCapabilities(address); } void Ieee80211Mib::clearAssociationIds() { + checkStateMutation(); + stateChangePending |= !bssAccessPointData.stations.empty() || !bssAccessPointData.associationIds.empty(); 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 691257c2f07..549e807e97b 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -8,9 +8,12 @@ #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" +#include "inet/linklayer/ieee80211/mib/Ieee80211VhtCapabilities.h" namespace inet { @@ -24,6 +27,8 @@ namespace ieee80211 { class INET_API Ieee80211Mib : public SimpleModule { public: + static simsignal_t bssStateChangedSignal; + enum Mode { INFRASTRUCTURE, INDEPENDENT, @@ -63,8 +68,12 @@ class INET_API Ieee80211Mib : public SimpleModule public: bool valid = false; Ieee80211HtCapabilities advertisedCapabilities; - Ieee80211NegotiatedHtCapabilities negotiatedCapabilities; - uint64_t generation = 0; + std::shared_ptr negotiatedCapabilities; + }; + + struct PeerVhtState { + Ieee80211VhtCapabilities advertisedCapabilities; + Ieee80211VhtOperation operation; }; public: @@ -72,6 +81,7 @@ class INET_API Ieee80211Mib : public SimpleModule Mode mode = static_cast(-1); bool qos = false; + private: BssData bssData; BssStationData bssStationData; BssAccessPointData bssAccessPointData; @@ -79,18 +89,39 @@ 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; - 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; + std::map peerVhtStates; protected: 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,18 +131,40 @@ 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); + // 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; } + 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(); + 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 70474b3d95b..138515cfa0a 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -19,10 +19,15 @@ 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. 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"); 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/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/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/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc index 30eaa47132d..31d98a3cab4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc @@ -312,7 +312,7 @@ const ITransmission *Ieee80211LayeredOfdmTransmitter::createTransmission(const I // TODO: compute channel const simtime_t preambleDuration = mode->getPreambleLength(); const simtime_t headerDuration = mode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = mode->getDataMode()->getDuration(packet->getDataLength()); + const simtime_t dataDuration = mode->getDataDuration(packet->getDataLength()); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, packetModel, bitModel, symbolModel, sampleModel, analogModel, mode, nullptr); } @@ -331,4 +331,3 @@ Ieee80211LayeredOfdmTransmitter::~Ieee80211LayeredOfdmTransmitter() } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/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/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/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index aa615b7d19e..cea690cf72d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -42,8 +42,13 @@ class INET_API IIeee80211DataMode : public cObject, public IPrintableObject virtual bps getGrossBitrate() const = 0; virtual b getPaddingLength(b dataLength) const = 0; virtual b getCompleteLength(b dataLength) const = 0; + // Returns the raw duration of the encoded data symbol train. PPDU-format + // rules may round this duration at the enclosing mode level. virtual const simtime_t getDuration(b dataLength) const = 0; virtual const simtime_t getSymbolInterval() const = 0; + // Returns the guard interval used by the data symbols, or -1 when the PHY + // has no meaningful guard interval (for example, non-OFDM modes). + virtual const simtime_t getGuardInterval() const = 0; virtual const IModulation *getModulation() const = 0; virtual int getNumberOfSpatialStreams() const = 0; }; @@ -55,9 +60,14 @@ 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; + // 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; @@ -68,6 +78,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; @@ -84,4 +97,3 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject } // namespace inet #endif - 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 319128cce20..fb630c9e84d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -161,6 +161,16 @@ unsigned int Ieee80211HtPreambleMode::computeNumberOfHTLongTrainings(unsigned in return numberOfSpaceTimeStreams == 3 ? 4 : numberOfSpaceTimeStreams; } +const simtime_t Ieee80211HtPreambleMode::getDurationBeforeHeader() const +{ + if (preambleFormat == HT_PREAMBLE_MIXED) + return getNonHTShortTrainingSequenceDuration() + getNonHTLongTrainingFieldDuration() + legacySignalMode->getDuration(); + else if (preambleFormat == HT_PREAMBLE_GREENFIELD) + return getHTGreenfieldShortTrainingFieldDuration() + getFirstHTLongTrainingFieldDuration(); + else + throw cRuntimeError("Unknown preamble format"); +} + const simtime_t Ieee80211HtPreambleMode::getDuration() const { // 20.3.7 Mathematical description of signals @@ -178,12 +188,8 @@ const simtime_t Ieee80211HtPreambleMode::getDuration() const bps Ieee80211HtSignalMode::computeGrossBitrate() const { unsigned int numberOfCodedBitsPerSymbol = modulation->getSubcarrierModulation()->getCodeWordSize() * getNumberOfDataSubcarriers(); - if (guardIntervalType == HT_GUARD_INTERVAL_LONG) - return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); - else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) - return bps(numberOfCodedBitsPerSymbol / getShortGISymbolInterval()); - else - throw cRuntimeError("Unknown guard interval type"); + // IEEE Std 802.11-2024, 19.3.11.11.6: the short GI applies only to the Data field. + return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); } bps Ieee80211HtSignalMode::computeNetBitrate() const @@ -239,6 +245,16 @@ bps Ieee80211HtModeBase::getGrossBitrate() const return grossBitrate; } +const simtime_t Ieee80211HtDataMode::getGuardInterval() const +{ + if (guardIntervalType == HT_GUARD_INTERVAL_LONG) + return getGIDuration(); + else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) + return getShortGIDuration(); + else + throw cRuntimeError("Unknown guard interval type"); +} + int Ieee80211HtModeBase::getNumberOfDataSubcarriers() const { return Ieee80211Htmcs::getNumberOfDataSubcarriers(bandwidth, mcsIndex); @@ -285,6 +301,22 @@ const simtime_t Ieee80211HtDataMode::getDuration(b dataLength) const return numberOfSymbols * getSymbolInterval(); } +const simtime_t Ieee80211HtMode::getDuration(b dataLength) const +{ + auto dataDuration = dataMode->getDuration(dataLength); + if (preambleMode->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + { + // IEEE Std 802.11-2024, 19.4.3, Eq. (19-90): mixed-format short-GI + // Data airtime is rounded up to a 4 us boundary. Eq. (19-92) leaves + // greenfield short-GI Data airtime at its raw 3.6 us symbol duration. + auto longGiSymbolInterval = dataMode->getDFTPeriod() + dataMode->getGIDuration(); + auto numberOfLongGiSymbols = (dataDuration.raw() + longGiSymbolInterval.raw() - 1) / longGiSymbolInterval.raw(); + dataDuration = SimTime::fromRaw(numberOfLongGiSymbols * longGiSymbolInterval.raw()); + } + return preambleMode->getDuration() + dataDuration; +} + const simtime_t Ieee80211HtMode::getSlotTime() const { if (centerFrequencyMode == BAND_2_4GHZ) @@ -326,7 +358,7 @@ Ieee80211HtCompliantModes::~Ieee80211HtCompliantModes() const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee80211Htmcs *mcsMode, Ieee80211HtMode::BandMode centerFrequencyMode, Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat, Ieee80211HtModeBase::GuardIntervalType guardIntervalType) { const char *name = ""; // TODO - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType); + auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), centerFrequencyMode, preambleFormat, guardIntervalType); auto mode = singleton.modeCache.find(htModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmModulation *modulation = nullptr; @@ -348,7 +380,7 @@ const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee802 const Ieee80211HtDataMode *dataMode = new Ieee80211HtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211HtPreambleMode *preambleMode = new Ieee80211HtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211HtMode *htMode = new Ieee80211HtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); + singleton.modeCache.emplace(htModeId, htMode); return htMode; } return mode->second; @@ -442,7 +474,9 @@ const DI Ieee80211HtmcsTable::htMcs29BW40MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs30BW40MHz([](){ return new Ieee80211Htmcs(30, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs31BW40MHz([](){ return new Ieee80211Htmcs(31, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Ieee80211HtCompliantCodes::htConvolutionalCode5_6, MHz(40));}); -const DI Ieee80211HtmcsTable::htMcs32BW40MHz([](){ return new Ieee80211Htmcs(32, &BpskModulation::singleton, &BpskModulation::singleton, &BpskModulation::singleton, &BpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); +// IEEE Std 802.11-2024, Table 19-35: optional MCS 32 is one BPSK stream. +// This corrects the previous incorrect 4-stream all-BPSK constructor. +const DI Ieee80211HtmcsTable::htMcs32BW40MHz([](){ return new Ieee80211Htmcs(32, &BpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs33BW20MHz([](){ return new Ieee80211Htmcs(33, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs34BW20MHz([](){ return new Ieee80211Htmcs(34, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(20));}); @@ -490,7 +524,9 @@ const DI Ieee80211HtmcsTable::htMcs72BW20MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs73BW20MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs74BW20MHz([](){ return new Ieee80211Htmcs(74, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs75BW20MHz([](){ return new Ieee80211Htmcs(75, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); -const DI Ieee80211HtmcsTable::htMcs76BW20MHz([](){ return new Ieee80211Htmcs(76, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); +// IEEE Std 802.11-2024, Table 19-38 (continued): MCS 76 uses 64-QAM for streams 1-3 and 16-QAM for stream 4. +// This corrects the previous incorrect QPSK modulation for the 4th stream. +const DI Ieee80211HtmcsTable::htMcs76BW20MHz([](){ return new Ieee80211Htmcs(76, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs33BW40MHz([](){ return new Ieee80211Htmcs(33, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs34BW40MHz([](){ return new Ieee80211Htmcs(34, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); @@ -536,7 +572,9 @@ const DI Ieee80211HtmcsTable::htMcs69BW40MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs70BW40MHz([](){ return new Ieee80211Htmcs(70, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs71BW40MHz([](){ return new Ieee80211Htmcs(71, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs72BW40MHz([](){ return new Ieee80211Htmcs(72, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); -const DI Ieee80211HtmcsTable::htMcs73BW40MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); +// IEEE Std 802.11-2024, Table 19-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));}); @@ -544,4 +582,3 @@ const DI Ieee80211HtmcsTable::htMcs76BW40MHz([](){ return new Ie } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 9184765f083..b35858ad69d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -153,6 +153,7 @@ class INET_API Ieee80211HtPreambleMode : public IIeee80211PreambleMode, public I virtual const simtime_t getSecondAndSubsequentHTLongTrainingFielDuration() const { return 4E-6; } // HT-LTFs, s = 2,3,..,n virtual unsigned int getNumberOfHtLongTrainings() const { return numberOfHTLongTrainings; } + virtual const simtime_t getDurationBeforeHeader() const; virtual const simtime_t getDuration() const override; virtual Ptr createPreamble() const override { return makeShared(); } @@ -218,7 +219,8 @@ class INET_API Ieee80211HtDataMode : public IIeee80211DataMode, public Ieee80211 virtual bps getGrossBitrate() const override { return Ieee80211HtModeBase::getGrossBitrate(); } virtual const Ieee80211Htmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211HtCode *getCode() const { return modulationAndCodingScheme->getCode(); } - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + virtual const simtime_t getGuardInterval() const override; + virtual const simtime_t getSymbolInterval() const override { return getDFTPeriod() + getGuardInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; @@ -246,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(); } @@ -263,7 +266,10 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataLength); } + virtual const simtime_t getDuration(b dataLength) const override; + virtual const simtime_t getPreambleDuration() const override { return preambleMode->getDurationBeforeHeader(); } + virtual const simtime_t getHeaderDuration() const override { return preambleMode->getDuration() - getPreambleDuration(); } + virtual const simtime_t getDataDuration(b dataLength) const override { return getDuration(dataLength) - preambleMode->getDuration(); } }; // A specification of the high-throughput (HT) physical layer (PHY) @@ -462,7 +468,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/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/Ieee80211ModeBase.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h index dd5d6fdd0ca..cb0fa7f3306 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h @@ -21,8 +21,12 @@ 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(); } + 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/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 5a53166e2cc..8fb73eacf29 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,8 +362,75 @@ 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), - Ieee80211ModeSet("ac", { + }, 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) }, { 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,17 +746,39 @@ 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) : + 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()); @@ -578,6 +809,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 @@ -585,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); } @@ -598,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) { @@ -617,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++) @@ -649,6 +888,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); @@ -660,44 +1002,114 @@ 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::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(const IIeee80211Mode *mode) const { - return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams); + auto result = findMode(mode); + if (result == nullptr) + throw cRuntimeError("Unknown mode in operation mode: '%s'", getName()); + return result; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode *mode) const { - for (size_t index = 0; index < entries.size(); index++) { - auto mode = entries[index].mode; - auto dataMode = mode->getDataMode(); - auto bitrate = dataMode->getNetBitrate(); - if (minBitrate <= bitrate && bitrate <= maxBitrate && - (std::isnan(bandwidth.get()) || dataMode->getBandwidth() == bandwidth) && - (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams)) - { - return entries[index].mode; - } + 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::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(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); + 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, 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}) { + 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) + { + 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 result; +} + +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, requireUnique); 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, bool requireUnique) const { - const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams); + const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval, requireUnique); 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 +1127,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 +1176,127 @@ 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 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 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) @@ -806,4 +1327,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 e14627dbcbe..8b5e1dd8805 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -28,12 +28,13 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject VHT, }; - protected: class INET_API Entry { public: bool isMandatory; const IIeee80211Mode *mode; bool isLegacyOperational = false; + // Prefer historical catalog entries when no GI qualifier is supplied. + bool isPreferredForUnqualifiedLookup = true; }; struct EntryNetBitrateComparator { @@ -52,18 +53,28 @@ 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; + 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; } @@ -85,23 +96,52 @@ 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() + // 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; - - 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; + + // 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; + // 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; 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; + // 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; + // 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); @@ -125,4 +165,3 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h index 6ca84f780f3..608943efba3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h @@ -124,6 +124,7 @@ class INET_API Ieee80211OfdmDataMode : public IIeee80211DataMode, public Ieee802 virtual b getPaddingLength(b dataLength) const override; virtual b getCompleteLength(b dataLength) const override; virtual const simtime_t getDuration(b dataLength) const override; + virtual const simtime_t getGuardInterval() const override { return getGIDuration(); } const Ieee80211OfdmCode *getCode() const { return code; } virtual const simtime_t getSymbolInterval() const override { return Ieee80211OfdmTimingRelatedParametersBase::getSymbolInterval(); } @@ -265,4 +266,3 @@ class INET_API Ieee80211OfdmCompliantModes } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index 89b142e37cb..055bc297c05 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 @@ -312,6 +316,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)) @@ -639,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) @@ -672,10 +700,14 @@ 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 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; @@ -693,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, const Ieee80211VhtMode *>(htModeId, htMode)); + singleton.modeCache.emplace(vhtModeId, htMode); return htMode; } return mode->second; @@ -1072,4 +1104,3 @@ const DI Ieee80211VhtmcsTable::vhtMcs9BW160MHzNss8([](){ return } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index aa4aa3639b0..d1a820e8eca 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h @@ -96,6 +96,8 @@ class INET_API Ieee80211VhtSignalMode : public IIeee80211HeaderMode, public Ieee virtual b getLength() const override; virtual bps getNetBitrate() const override { return Ieee80211VhtModeBase::getNetBitrate(); } virtual bps getGrossBitrate() const override { return Ieee80211VhtModeBase::getGrossBitrate(); } + // IEEE Std 802.11-2024, Table 21-5: VHT-SIG uses the long-GI symbol + // interval independently of the data field's selected guard interval. virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulation; } virtual const Ieee80211VhtCode *getCode() const { return code; } @@ -148,6 +150,7 @@ class INET_API Ieee80211VhtPreambleMode : public IIeee80211PreambleMode, public virtual const simtime_t getSecondAndSubsequentHTLongTrainingFielDuration() const { return 4E-6; } // HT-LTFs, s = 2,3,..,n virtual unsigned int getNumberOfHtLongTrainings() const { return numberOfHTLongTrainings; } + virtual const simtime_t getDurationBeforeHeader() const; virtual const simtime_t getDuration() const override; virtual Ptr createPreamble() const override { return makeShared(); } @@ -237,13 +240,17 @@ class INET_API Ieee80211VhtDataMode : public IIeee80211DataMode, public Ieee8021 virtual bps getGrossBitrate() const override { return Ieee80211VhtModeBase::getGrossBitrate(); } virtual const Ieee80211Vhtmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211VhtCode *getCode() const { return modulationAndCodingScheme->getCode(); } - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + virtual const simtime_t getGuardInterval() const override; + // 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(); } }; class INET_API Ieee80211VhtMode : public Ieee80211ModeBase { public: + virtual int getVhtMcsIndex() const override { return dataMode->getMcsIndex(); } enum BandMode { BAND_2_4GHZ, BAND_5GHZ @@ -280,7 +287,10 @@ class INET_API Ieee80211VhtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataBitLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataBitLength); } + virtual const simtime_t getDuration(b dataBitLength) const override { return preambleMode->getDuration() + getDataDuration(dataBitLength); } + virtual const simtime_t getPreambleDuration() const override { return preambleMode->getDurationBeforeHeader(); } + virtual const simtime_t getHeaderDuration() const override { return preambleMode->getDuration() - getPreambleDuration(); } + virtual const simtime_t getDataDuration(b dataBitLength) const override; }; // A specification of the high-throughput (HT) physical layer (PHY) @@ -679,7 +689,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/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..5c0ff55e7e9 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -8,8 +8,15 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" #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" @@ -44,6 +51,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 +76,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 +104,73 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) void Ieee80211Radio::setModeSet(const Ieee80211ModeSet *modeSet) { - Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); - Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); - ieee80211Transmitter->setModeSet(modeSet); - ieee80211Receiver->setModeSet(modeSet); - EV << "Changing radio mode set to " << modeSet << endl; - receptionTimer = nullptr; - emit(listeningChangedSignal, 0); + 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); + abortIncompatibleReception(); + if (modeSetCoordinator != nullptr) + modeSetCoordinator->completeModeSetChange(modeSet); + else if (modeSet != nullptr && getComponentType() != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); + if (getComponentType() != nullptr) + 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(); +} + +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) && + check_and_cast(receiver)->isHtChannelWidthSupported(channelWidth); } void Ieee80211Radio::setMode(const IIeee80211Mode *mode) @@ -106,8 +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; - receptionTimer = nullptr; - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setBand(const IIeee80211Band *band) @@ -117,13 +189,14 @@ 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()); emit(radioChannelChangedSignal, channel->getChannelNumber(), &details); } - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setChannel(const Ieee80211Channel *channel) @@ -135,10 +208,11 @@ 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); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setChannelNumber(int newChannelNumber) @@ -148,10 +222,11 @@ 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); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::insertFcs(const Ptr& phyHeader) const diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 087703e87f5..3d526363aea 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,16 @@ 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); + void abortIncompatibleReception(); + virtual void handleUpperCommand(cMessage *message) override; virtual void insertFcs(const Ptr& phyHeader) const; @@ -48,7 +56,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..d4683437183 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,10 +23,12 @@ 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 opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-2.4Ghz)", "p", "ac") = default("g(mixed)"); // Operation mode + 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)", "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 4c9d2ecd6d1..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 @@ -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..f195387a582 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; @@ -37,11 +38,13 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override; + const Ieee80211ModeSet *getModeSet() const { return modeSet; } virtual void setModeSet(const Ieee80211ModeSet *modeSet); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); 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..8f2e716167a 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,10 +22,10 @@ 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"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); int channelNumber; modulation = default("BPSK"); // TODO this is simply wrong @@ -32,4 +34,3 @@ module Ieee80211Receiver extends NarrowbandReceiverBase *.opMode = this.opMode; @class(Ieee80211Receiver); } - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 6c76381a790..465940cfefa 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(); } @@ -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; } @@ -151,9 +174,12 @@ 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())); + 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); } @@ -161,4 +187,3 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index 05b6f29194c..ce0e61090e5 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; @@ -38,14 +39,22 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase virtual const IIeee80211Mode *computeTransmissionMode(const Packet *packet) const; virtual const Ieee80211Channel *computeTransmissionChannel(const Packet *packet) const; + // Re-selects the current mode only when bitrate, bandwidth, NSS, and GI + // remain compatible. Use setModeSetAndMode for an explicit transition. + const Ieee80211ModeSet *getModeSet() const { return modeSet; } + const IIeee80211Mode *getMode() const { return mode; } + // 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); 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..ddd1c96d80b 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,10 +16,10 @@ 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"); + 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/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/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 64abe9fcac0..4c87e03aa02 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, 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 @@ -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, 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 @@ -655,10 +655,11 @@ /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, 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, 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, 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 330453edd7b..e9673a1dd4d 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 @@ -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, 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 +/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 -/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 @@ -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, 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 @@ -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, 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 @@ -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, 9032-d5ab/tplx;6276-d05a/~tNl;7526-602e/tyf, PASS, wireless Ipv4 diff --git a/tests/fingerprint/store.json b/tests/fingerprint/store.json index 321b6b5c23a..072c6784b34 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": "e382-0a32", "timestamp": 1681992800.58698, "itervars": "$repetition==0" }, @@ -35635,7 +35635,7 @@ "sim_time_limit": "100s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "1108-9845", + "fingerprint": "45db-b12b", "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/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/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/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/Ieee80211BlockAckResponseCorrelation_1.test b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test new file mode 100644 index 00000000000..9f991fe50c7 --- /dev/null +++ b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test @@ -0,0 +1,508 @@ +%description: +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 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. + +%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" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.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; + 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; } + 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(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); } + 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 bool isReceptionInProgress() override { return false; } + + 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 *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++; + lastFinishedAt = simTime(); + } + + virtual void scheduleStartRxTimer(simtime_t timeout) override + { + scheduleAfter(timeout, startRxTimer); + } + + public: + QosAckHandler ackHandler; + int transmittedFrames = 0; + int acceptedResponses = 0; + int failedFrames = 0; + int finishedSequences = 0; + simtime_t lastFinishedAt = SIMTIME_ZERO; + + 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, SequenceNumber startingSequenceNumber = CURRENT_SSN) + { + Enter_Method("injectBlockAck"); + Ptr blockAck; + if (compressed) { + auto compressedBlockAck = makeShared(); + compressedBlockAck->setTidInfo(tid); + compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + compressedBlockAck->setBlockAckBitmap(BitVector(std::vector(8, 255))); + blockAck = compressedBlockAck; + } + else { + auto basicBlockAck = makeShared(); + basicBlockAck->setTidInfo(tid); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + for (int i = 0; i < 64; i++) + basicBlockAck->setBlockAckBitmap(i, BitVector(std::vector(2, 255))); + blockAck = basicBlockAck; + } + blockAck->setReceiverAddress(localAddress); + blockAck->setTransmitterAddress(transmitterAddress); + auto packet = new Packet("injectedBlockAck", blockAck); + 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); + +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 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); + + 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); + + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, compressed); + ASSERT(!hcf->isResponseWaitActive()); + ASSERT(!hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + 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; + 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); + + 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()); + 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); + 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, SSN, and variant until match or timeout.\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 = 3ms +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 BAR response waits scoped by expected type, peer, TID, SSN, and variant until match or timeout. diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test new file mode 100644 index 00000000000..7383d8ed01f --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -0,0 +1,44 @@ +%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)" +**.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 +Processing Addba Request from + +%contains: stdout +Processing transmitted frame CompressedBlockAckReq as originator in frame sequence. + +%contains: stdout +Ieee80211CompressedBlockAck has arrived diff --git a/tests/module/Ieee80211CompressedBlockAckValidation.test b/tests/module/Ieee80211CompressedBlockAckValidation.test new file mode 100644 index 00000000000..a136a9fb4e1 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckValidation.test @@ -0,0 +1,364 @@ +%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: + RecipientBlockAckAgreementHandler lifetimeHandler; + RecipientBlockAckAgreement *lifetimeAgreement = nullptr; + 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; + policy.applyModeSet(physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); + 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; + } + + 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()); + 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); + checkRecipient(false, true, true, 0); + 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"; + } +}; +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. + +%contains: stdout +Rejected BARs preserve inactivity deadlines; valid BARs renew them. diff --git a/tests/module/Ieee80211ConfigurationContracts_1.test b/tests/module/Ieee80211ConfigurationContracts_1.test new file mode 100644 index 00000000000..ae36857923f --- /dev/null +++ b/tests/module/Ieee80211ConfigurationContracts_1.test @@ -0,0 +1,201 @@ +%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 IIeee80211ModeSetProvider +{ + protected: + const Ieee80211ModeSet *catalog = nullptr; + virtual void initialize() override { catalog = Ieee80211ModeSet::getModeSet("g(mixed)"); } + public: + virtual const Ieee80211ModeSet *getConfiguredModeSet() const override { return catalog; } +}; +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.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 IIeee80211ModeSetProvider { 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/Ieee80211ConfiguredResponseRateSelection.test b/tests/module/Ieee80211ConfiguredResponseRateSelection.test new file mode 100644 index 00000000000..c3882d4a761 --- /dev/null +++ b/tests/module/Ieee80211ConfiguredResponseRateSelection.test @@ -0,0 +1,207 @@ +%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/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" +#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 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 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)); + 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)); + 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)); + 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/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/Ieee80211HcfAddbaRedDrop_1.test b/tests/module/Ieee80211HcfAddbaRedDrop_1.test new file mode 100644 index 00000000000..160d1f7d91f --- /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, policy); + 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/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/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/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test new file mode 100644 index 00000000000..6afe11774f8 --- /dev/null +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -0,0 +1,129 @@ +%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->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; + 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.examples.wireless.lan80211.Lan80211; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; + +module HtPreambleCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtPreambleCheckingRadio); +} + +network TestHtGreenfield extends Lan80211 +{ + parameters: + numHosts = default(2); +} + +%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/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test new file mode 100644 index 00000000000..47750463139 --- /dev/null +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -0,0 +1,205 @@ +%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 greenfieldStaMixedCtsCount = 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->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; + + 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) && isHtMixed && isHost0) + greenfieldStaMixedCtsCount++; + 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 || 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"; + } +}; + +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" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B + +[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]* 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/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test new file mode 100644 index 00000000000..ccc530ad86d --- /dev/null +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -0,0 +1,130 @@ +%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->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) + 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.examples.wireless.lan80211.Lan80211; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module HtMixedCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtMixedCheckingRadio); +} + +network TestHtMixedRuntime extends Lan80211 +{ + parameters: + numHosts = default(2); +} + +%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/Ieee80211MgmtApCancellation_1.test b/tests/module/Ieee80211MgmtApCancellation_1.test new file mode 100644 index 00000000000..d9df4e454b6 --- /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->setPeerAssociationStatus(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->getBssAccessPointData().associationIds.find(address) != mib->getBssAccessPointData().associationIds.end(); + } + + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const + { + return mib->getPeerAssociationStatus(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/Ieee80211MgmtApChannelChange_1.test b/tests/module/Ieee80211MgmtApChannelChange_1.test index 324c0369224..cd306703a90 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,12 +234,25 @@ 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)); 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); @@ -253,9 +266,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 +278,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 +296,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 +328,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 +342,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; @@ -339,11 +354,27 @@ 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. - 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); @@ -426,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 @@ -459,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/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..11e64ff1253 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); @@ -46,7 +48,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 +97,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 @@ -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++) @@ -186,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/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..5479bc0e490 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,12 @@ 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); } + 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: virtual void frameTransmissionFinished(const Packet *frame, FrameTransmissionStatus status) override @@ -105,7 +110,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); @@ -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/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/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/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..4a4d1a69bf6 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,16 +504,16 @@ 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); 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); @@ -539,9 +543,11 @@ 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); - staMib->setPeerHtCapabilities(address, staMib->localHtCapabilities, staMib->getHtOperation()); + 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/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/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/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/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/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. 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. 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/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/module/QosRateSelectionNullModeSetInitialization.test b/tests/module/QosRateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..e0f7d8a0175 --- /dev/null +++ b/tests/module/QosRateSelectionNullModeSetInitialization.test @@ -0,0 +1,69 @@ +%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 NullModeSetProvider : public cSimpleModule, public IIeee80211ModeSetProvider +{ + public: + const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const override { return nullptr; } +}; +Define_Module(NullModeSetProvider); + +class NullModeSetQosRateSelection : public QosRateSelection +{ + protected: + void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) + ModeSetModuleBase::initialize(stage); + else if (stage == INITSTAGE_LINK_LAYER) + QosRateSelection::initialize(stage); + } +}; +Define_Module(NullModeSetQosRateSelection); + +} // namespace QosRateSelectionNullModeSetInitialization + +%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 = ""; +} + +network TestQosRateSelectionNullModeSetInitialization +{ + submodules: + provider: NullModeSetProvider; + 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 +Configured IEEE 802.11 mode catalog is unavailable diff --git a/tests/module/RateSelectionNullModeSetInitialization.test b/tests/module/RateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..e3aa82d74ee --- /dev/null +++ b/tests/module/RateSelectionNullModeSetInitialization.test @@ -0,0 +1,69 @@ +%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 NullModeSetProvider : public cSimpleModule, public IIeee80211ModeSetProvider +{ + public: + const physicallayer::Ieee80211ModeSet *getConfiguredModeSet() const override { return nullptr; } +}; +Define_Module(NullModeSetProvider); + +class NullModeSetRateSelection : public RateSelection +{ + protected: + void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) + ModeSetModuleBase::initialize(stage); + else if (stage == INITSTAGE_LINK_LAYER) + RateSelection::initialize(stage); + } +}; +Define_Module(NullModeSetRateSelection); + +} // namespace RateSelectionNullModeSetInitialization + +%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 = ""; +} + +network TestRateSelectionNullModeSetInitialization +{ + submodules: + provider: NullModeSetProvider; + 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 +Configured IEEE 802.11 mode catalog is unavailable 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/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. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test new file mode 100644 index 00000000000..467c35bfe65 --- /dev/null +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -0,0 +1,6848 @@ +%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/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" +#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/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" +#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/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/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" +#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" +#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 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 bool isPeerCompressedBlockAckSupported(const MacAddress&) 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 bool isPeerCompressedBlockAckSupported(const MacAddress&) const override { return false; } + virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } +}; + +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 *agreement) const override { return blockAckNeeded && agreement != nullptr; } + virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } + virtual simtime_t computeBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } +}; + +class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback +{ + public: + Packet *managementPacket = nullptr; + 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; + 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()); } + 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(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 bool releaseBlockAckAgreementFrames(MacAddress, Tid) override { return false; } + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { + 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 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: + using OriginatorBlockAckAgreementHandler::buildAddbaRequest; + 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; } + 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; } + 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: + 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 TestRecipientHandler : public RecipientBlockAckAgreementHandler +{ + public: + 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: + 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)); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; + +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 TestImmediateRemovalQueue : public TestPacketQueue +{ + public: + PacketRemovalReason removalReason = PacketRemovalReason::DROPPED; + + virtual void enqueuePacket(Packet *packet) override { + notifyPacketRemoved(packet, removalReason); + delete packet; + } +}; + +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: + 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 TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + +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 TestOrderedSignalListener : public cListener +{ + public: + std::vector signals; + + virtual void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { signals.push_back(signal); } +}; + +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 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: + 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 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: + virtual ~TestHcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + int numProcessedDroppedSetupFrames = 0; + int numResumedEligibleChannelAccess = 0; + int numCancelledAddbaTransactions = 0; + int numCancelledBlockAckTeardowns = 0; + std::vector cancelledBlockAckTeardownGenerationIds; + int numRebuildEligibilityCalls = 0; + int numTransmittedControlResponses = 0; + bool managementProcessedDuringBlockAckExpiry = false; + 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 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; + 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, 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 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); } + 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); + } + void processInactivityTimeout() { + if (inactivityTimer == nullptr) + inactivityTimer = new cMessage("BlockAckInactivityTimer"); + handleMessage(inactivityTimer); + } + + protected: + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numTransmittedControlResponses++; + delete responsePacket; + } + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementProcessedDuringBlockAckExpiry |= blockAckInactivityExpiryInProgress; + 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 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(BlockAckAgreementRole, simtime_t) override {} + 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: + 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 +{ + 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: + virtual std::vector computeFragmentSizes(Packet *) override { return { 8, 5 }; } +}; + +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: + 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; + } +}; + +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; + bool cancellationRequested = 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 bool processResponse(Packet *) override { return true; } + 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 +{ + public: + int numReleasedChannels = 0; + + 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++; } +}; + +class TestDcf : public Dcf +{ + public: + virtual ~TestDcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + 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 { + 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(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiverAddress); + header->setTid(tid); + 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()->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; +} + +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(); + response->setTransmitterAddress(transmitterAddress); + response->setTid(tid); + response->setDialogToken(dialogToken); + response->setStatusCode(statusCode); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(0); + 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. +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); + module->par("dropperClass").setStringValue(dropperClass); + module->callInitialize(); + 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); + 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); +} + +// 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)); + +// 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()->setGenerationId(77); + finalDelbaPacket.addTag()->setGenerationId(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.cancelledTeardownGenerationIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &finalDelbaPacket); + + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); + firstDelbaPacket.getTagForUpdate()->setGenerationId(78); + ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + 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 +// 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 = findFragmentedActionContext(packet); + 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 = findFragmentedActionContext(packet); + 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 completeDelba = makeShared(); + completeDelba->setReceiverAddress(peer1); + completeDelba->setTid(6); + completeDelba->setInitiator(true); + completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); + auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); + completeDelbaPacket->addTag()->setGenerationId(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, findFragmentedActionContext(finalPacket))); + 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.numCancelledBlockAckTeardowns == 1); + 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 +// 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 = findFragmentedActionContext(packet); + 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.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.numCancelledBlockAckTeardowns == 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()->setGenerationId(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; +} + +// 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 +// 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); + transmittedDelba->setInitiator(true); + transmittedDelba->setMoreFragments(true); + auto transmittedFinalDelba = staticPtrCast(transmittedDelba->dupShared()); + transmittedFinalDelba->setMoreFragments(false); + transmittedFinalDelba->setFragmentNumber(1); + Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); + 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) + 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. +// Transaction-scoped cancellation removes all exact siblings while retaining +// the dropped fragment and an unrelated transaction. +{ + auto fragmentedHandler = new TestOriginatorHandler(); + 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); + auto unfragmentedRequestHeader = unfragmentedRequestPacket->removeAtFront(); + unfragmentedRequestHeader->setSequenceNumber(SequenceNumberCyclic(50)); + unfragmentedRequestPacket->insertAtFront(unfragmentedRequestHeader); + 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); + 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); + auto siblingFragment = fragments->at(1); + TestPacketQueue fragmentedPendingQueue; + 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); + 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()); + 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); +} + +// 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. +{ + 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); + 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); + 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); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, 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 firstAgreementCreationTime = simTime(); + auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); + ASSERT(!firstReception.duplicate); + ASSERT(firstReception.completeFrames.empty()); + ASSERT(firstReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(firstReception.completeHeader, 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() == 1); + ASSERT(firstResponse->getAMsduSupported()); + ASSERT(firstAgreement->getBlockAckTimeoutValue() == 1); + ASSERT(firstAgreement->getExpirationTime() == firstAgreementCreationTime + 1); + + 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()); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, 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); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, 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() == 1); + 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); + ASSERT(duplicateRejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateRejectedReception.completeHeader, 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()); + ASSERT(replacementReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(replacementReception.completeHeader, 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, 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(); + + // 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); + 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; + + // 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); + 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); + 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); + 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; +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 != 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. +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(); +} + +// 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); + ASSERT(immediateFrames.front().first == 100); + auto immediateFragments = immediateFrames.front().second; + 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); + ASSERT(barFrames.front().first == 201); + auto barFragments = barFrames.front().second; + assertFragmentOrder(barFragments); + 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 +// 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, AckPolicy ackPolicy) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + 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, BLOCK_ACK); + ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); + 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, NORMAL_ACK); + 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, BLOCK_ACK); + 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. +{ + 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()); + + 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(NORMAL_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); + 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(); +} + +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)); + 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); + + 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()->getGenerationId()); + heldTeardownHcf.cancelTeardown(true, peer1, 7, 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. +{ + 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. +{ + 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(); +} + +// 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(); +} + +// 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. +{ + 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; +} + +// 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. +{ + 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(); +} + +// 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. +{ + 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(); +} + +// 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); +} + +// 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); +} + +// 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; + + // 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 +// 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. +{ + 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 = 2; + TestCallback expiryCallback; + auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); + request->setBlockAckTimeoutValue(3); + auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + wait(2); + 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; +} + +// 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 = 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); + 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); + basicBlockAckReq->setTidInfo(12); + wait(1); + auto barReceptionTime = simTime(); + recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 2); + ASSERT(agreement->getExpirationTime() > creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 2); + + wait(4); + 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); + + // 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); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == originatorAgreement); + 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(originatorHandler.getActiveAgreement(peer1, 13) == nullptr); + 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); + +} + +// 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; +} + +// 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. +{ + 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. +{ + 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 +// 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); + + // 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); + 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 +ADDBA transaction and negotiated SSN integrity checks passed. 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()); 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. 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. +. 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. +. diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test new file mode 100644 index 00000000000..182118732dd --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -0,0 +1,973 @@ +%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/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/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" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure +{ + public: + 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: + using OriginatorBlockAckAgreementHandler::createAgreement; + using OriginatorBlockAckAgreementHandler::updateAgreement; +}; + +class TestOriginatorBlockAckAgreementPolicy : public OriginatorBlockAckAgreementPolicy +{ + public: + void configureCompressedBlockAckCapability(bool localSupport, std::initializer_list peerAddresses) + { + applyModeSet(physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); + 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) + { + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement); + } +}; + +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: + static bool isCompressedResponseNeeded(const Ptr& request, RecipientBlockAckAgreement *agreement) + { + return isCompressedBlockAckNeeded(request, agreement); + } +}; + +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) +{ + 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->setSequenceNumber(SequenceNumberCyclic(100)); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + 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: +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); + 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"; +} + +{ + 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(); + 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); + emptyAgreement.addbaResposneSent(); + emptyAgreement.setIsCompressedBlockAckSupported(true); + 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)); + 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"; +} + +{ + 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.dataFrameReceived(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.dataFrameReceived(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"; +} + +{ + 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"; +} + +{ + // 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); + + // 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)); + 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(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"; +} + +{ + 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)); + 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"; +} + +{ + 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)); + 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"; +} + +{ + 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); + 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.dataFrameReceived(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.dataFrameReceived(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)); + request->setFragmentNumber(1); + 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.setIsCompressedBlockAckSupported(true); + delayedAgreement.setIsDelayedBlockAckPolicySupported(true); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); + delayedAgreement.setIsDelayedBlockAckPolicySupported(false); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); + 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), 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()); + + // 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); + 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"; + + // 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"; +} + +{ + MacAddress receiverAddress("10:20:30:40:50:60"); + 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, 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, 0, 0); + 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)); + delete outstandingFrames[0]; + outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 0, true) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); + 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; + TestOriginatorBlockAckAgreementPolicy policy; + policy.configureCompressedBlockAckCapability(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); + delayedResponse->setBlockAckTimeoutValue(SIMTIME_ZERO); + 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"; +} + +{ + // 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"; +} + +{ + // 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 +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. +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. +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. +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. +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. +Basic and Compressed BAR rate selection is variant-agnostic. +Sparse Basic BAR limits waiting statuses to 64 positions across sequence wrap. +. diff --git a/tests/unit/Ieee80211GroupModeSelection_1.test b/tests/unit/Ieee80211GroupModeSelection_1.test new file mode 100644 index 00000000000..7c30f62dafc --- /dev/null +++ b/tests/unit/Ieee80211GroupModeSelection_1.test @@ -0,0 +1,58 @@ +%description: +Group selection preserves an equivalent external basic legacy mode, while +retaining active-catalog identity and bounding fallback by the requested rate. + +%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) == 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); +bool optionalRejected = false; +try { selectGroupAddressedMode(&optional, &externalSix); } +catch (const cRuntimeError&) { optionalRejected = true; } +ASSERT(optionalRejected); +GroupModeSet nonOperational({{true, six, false}, {true, twentyFour, true}}, 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); +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. diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index dee6a669f79..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; @@ -35,7 +36,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]); @@ -47,20 +48,22 @@ 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); // 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 +101,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/Ieee80211HtGreenfield_1.test b/tests/unit/Ieee80211HtGreenfield_1.test new file mode 100644 index 00000000000..d6a515de3e8 --- /dev/null +++ b/tests/unit/Ieee80211HtGreenfield_1.test @@ -0,0 +1,367 @@ +%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, b dataLength = B(0)) +{ + auto duration = mode->getDuration(dataLength); + auto preambleDuration = mode->getPreambleMode()->getDuration(); + auto modeledDataDuration = mode->getDataMode()->getDuration(dataLength); + auto dataDuration = mode->getDataDuration(dataLength); + bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; + auto headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : mode->getHeaderMode()->getDuration(); + ASSERT(preambleDuration >= SIMTIME_ZERO); + ASSERT(headerDuration >= SIMTIME_ZERO); + ASSERT(dataDuration >= SIMTIME_ZERO); + ASSERT(preambleDuration + headerDuration + dataDuration == duration); + if (dynamic_cast(mode) != nullptr || 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); +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)"); +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 new file mode 100644 index 00000000000..bf8a478b98f --- /dev/null +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -0,0 +1,585 @@ +%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; + +// Same OFDM tuple except for an absent GI, to isolate compatibility semantics. +class AbsentGiDataMode : public Ieee80211OfdmDataMode +{ + public: + AbsentGiDataMode(const Ieee80211OfdmDataMode& source) : Ieee80211OfdmDataMode(source) {} + virtual const simtime_t getGuardInterval() const override { return -1; } +}; + +class AbsentGiMode : public Ieee80211OfdmMode +{ + AbsentGiDataMode data; + public: + AbsentGiMode(const Ieee80211OfdmMode& source) : Ieee80211OfdmMode(source), data(*source.getDataMode()) {} + virtual const Ieee80211OfdmDataMode *getDataMode() const override { return &data; } +}; + +class TestIeee80211Transmitter : public Ieee80211Transmitter +{ + public: + 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; + setComponentType(cModuleType::get("inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio")); + } +}; + +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); + } + static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(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); + } + static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(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; +try { + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (cRuntimeError&) { + rejectedVhtGreenfieldBeforeMixed = true; +} +ASSERT(rejectedVhtGreenfieldBeforeMixed); + +const auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +ASSERT(modeSet->getNumModes() == 135); + +TestIeee80211Mgmt mgmt; +mgmt.notifyModeSet(modeSet); +const auto& htSupportedRates = mgmt.getSupportedRates(); +const double expectedHtSupportedRates[] = {1, 2, 5.5, 6, 11, 12, 24}; +ASSERT(htSupportedRates.numRates == 7); +for (int i = 0; i < htSupportedRates.numRates; i++) { + ASSERT(htSupportedRates.rate[i] == expectedHtSupportedRates[i]); + if (i > 0) + ASSERT(htSupportedRates.rate[i - 1] < htSupportedRates.rate[i]); +} + +mgmt.notifyModeSet(Ieee80211ModeSet::getModeSet("a")); +const auto& legacySupportedRates = mgmt.getSupportedRates(); +const double expectedLegacySupportedRates[] = {6, 12, 24, 9, 18, 36, 48, 54}; +ASSERT(legacySupportedRates.numRates == 8); +for (int i = 0; i < legacySupportedRates.numRates; i++) { + ASSERT(legacySupportedRates.rate[i] == expectedLegacySupportedRates[i]); + ASSERT(legacySupportedRates.basicRate[i] == (i < 3)); +} + +using Key = std::tuple; +std::map modes; +int mandatoryCount = 0; +for (int index = 0; index < modeSet->getNumModes(); index++) { + auto mode = dynamic_cast(modeSet->getMode(index)); + if (mode == nullptr) + continue; + auto dataMode = mode->getDataMode(); + int bandwidth = dataMode->getBandwidth() == MHz(20) ? 20 : dataMode->getBandwidth() == MHz(40) ? 40 : 0; + int mcs = dataMode->getMcsIndex(); + auto guardIntervalType = dataMode->getGuardIntervalType(); + ASSERT(bandwidth != 0); + ASSERT((bandwidth == 20 || bandwidth == 40) && 0 <= mcs && mcs <= 31); + ASSERT(modes.emplace(Key(bandwidth, mcs, guardIntervalType), mode).second); + + bool mustBeMandatory = bandwidth == 20 && mcs <= 7 && + guardIntervalType == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG; + ASSERT(modeSet->isMandatory(index) == mustBeMandatory); + if (modeSet->isMandatory(index)) + mandatoryCount++; +} +ASSERT(mandatoryCount == 8); + +for (int index = 0; index < modeSet->getNumModes(); index++) { + auto mode = modeSet->getMode(index); + auto slowerMode = modeSet->getSlowerMode(mode); + auto fasterMode = modeSet->getFasterMode(mode); + ASSERT(slowerMode == nullptr || slowerMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMode == nullptr || fasterMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); +} + +for (int bandwidth : {20, 40}) { + for (int mcs = 0; mcs <= 31; mcs++) { + auto longMode = modes.at(Key(bandwidth, mcs, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); + auto shortMode = modes.at(Key(bandwidth, mcs, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); + auto longData = longMode->getDataMode(); + auto shortData = shortMode->getDataMode(); + ASSERT(longData->getGuardInterval() == SimTime(800, SIMTIME_NS)); + ASSERT(shortData->getGuardInterval() == SimTime(400, SIMTIME_NS)); + ASSERT(longData->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(shortData->getSymbolInterval() == SimTime(3600, SIMTIME_NS)); + ASSERT(std::fabs(shortData->getNetBitrate().get() * 9 - longData->getNetBitrate().get() * 10) < 1); + + // IEEE Std 802.11-2024, 19.3.11.11.6: short GI is Data-only. + auto longSignal = longMode->getHeaderMode(); + auto shortSignal = shortMode->getHeaderMode(); + ASSERT(longSignal->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(shortSignal->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(longSignal->getDuration() == SimTime(8, SIMTIME_US)); + ASSERT(shortSignal->getDuration() == SimTime(8, SIMTIME_US)); + ASSERT(longSignal->getNetBitrate() == shortSignal->getNetBitrate()); + ASSERT(longSignal->getGrossBitrate() == shortSignal->getGrossBitrate()); + } +} +ASSERT(modes.find(Key(20, 32, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)) == modes.end()); + +auto mixedLong = modes.at(Key(20, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); +auto mixedShort = modes.at(Key(20, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +ASSERT(mixedLong->getDataMode()->getDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataMode()->getDuration(B(24)) == SimTime(32400, SIMTIME_NS)); +ASSERT(mixedShort->getDataMode()->getDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedLong->getDataMode()->getDuration(B(30)) == SimTime(44, SIMTIME_US)); +ASSERT(mixedShort->getDataMode()->getDuration(B(30)) == SimTime(39600, SIMTIME_NS)); + +// IEEE Std 802.11-2024, 19.4.3: Eq. (19-90) rounds mixed-format +// short-GI Data to 4 us, while Eq. (19-92) keeps greenfield Data raw. +ASSERT(mixedShort->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataDuration(B(30)) == SimTime(40, SIMTIME_US)); + +// Preserve representative historical optional-rate timing and bitrate values. +auto mixedShortMcs8 = modes.at(Key(20, 8, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto mixedShortMcs0Bw40 = modes.at(Key(40, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +ASSERT(std::fabs(mixedShortMcs8->getDataMode()->getNetBitrate().get() - 14.4444444444444e6) < 1); +ASSERT(mixedShortMcs8->getDataMode()->getDuration(B(24)) == SimTime(18, SIMTIME_US)); +ASSERT(mixedShortMcs8->getDataDuration(B(24)) == SimTime(20, SIMTIME_US)); +ASSERT(mixedShortMcs8->getDuration(B(24)) == SimTime(60, SIMTIME_US)); +ASSERT(mixedShortMcs0Bw40->getDataMode()->getNetBitrate() == Mbps(15)); +ASSERT(mixedShortMcs0Bw40->getDataMode()->getDuration(B(24)) == SimTime(14400, SIMTIME_NS)); +ASSERT(mixedShortMcs0Bw40->getDataDuration(B(24)) == SimTime(16, SIMTIME_US)); +ASSERT(mixedShortMcs0Bw40->getDuration(B(24)) == SimTime(52, SIMTIME_US)); +auto greenfieldShort = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldShort->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(greenfieldShort->getDataMode()->getNumberOfSpatialStreams() == 2); +ASSERT(greenfieldShort->getDataDuration(B(24)) == SimTime(18, SIMTIME_US)); +ASSERT(greenfieldShort->getDataDuration(B(30)) == SimTime(21600, SIMTIME_NS)); + +auto mixedLongOneSymbol = modes.at(Key(20, 15, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); +auto mixedShortOneSymbol = modes.at(Key(20, 15, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto greenfieldShortOneSymbol = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +for (auto mode : {mixedLongOneSymbol, mixedShortOneSymbol, greenfieldShortOneSymbol}) { + auto dataDuration = mode->getDataDuration(B(24)); + ASSERT(dataDuration >= SIMTIME_ZERO); + ASSERT(mode->getDuration(B(24)) == mode->getPreambleDuration() + mode->getHeaderDuration() + dataDuration); +} +ASSERT(mixedLongOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(mixedShortOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getDataDuration(B(24)) == SimTime(3600, SIMTIME_NS)); +ASSERT(mixedShortOneSymbol->getPreambleDuration() == SimTime(20, SIMTIME_US)); +ASSERT(mixedShortOneSymbol->getHeaderDuration() == SimTime(20, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getPreambleDuration() == SimTime(16, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getHeaderDuration() == SimTime(12, SIMTIME_US)); + +auto unspecified65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1)); +auto long65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1, SimTime(800, SIMTIME_NS))); +auto short65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1, SimTime(400, SIMTIME_NS))); +ASSERT(unspecified65->getDataMode()->getMcsIndex() == 7); +ASSERT(unspecified65->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(long65->getDataMode()->getMcsIndex() == 7); +ASSERT(short65->getDataMode()->getMcsIndex() == 6); + +TestIeee80211Transmitter transmitter; +transmitter.setModeSet(modeSet); +transmitter.setMode(short65); +ASSERT(transmitter.getSelectedMode() == short65); +bool rejectedModeOutsideSet = false; +try { + transmitter.setMode(greenfieldShortOneSymbol); +} +catch (cRuntimeError&) { + rejectedModeOutsideSet = true; +} +ASSERT(rejectedModeOutsideSet); +ASSERT(transmitter.getSelectedMode() == short65); + +auto unspecified135 = dynamic_cast(modeSet->getMode(Mbps(135), MHz(40), 1)); +ASSERT(unspecified135->getDataMode()->getMcsIndex() == 6); +ASSERT(unspecified135->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(modeSet->findMode(Mbps(65), MHz(20), 1, SimTime(600, SIMTIME_NS)) == nullptr); +const auto legacyOfdmModeSet = Ieee80211ModeSet::getModeSet("a"); +ASSERT(legacyOfdmModeSet->findMode(Mbps(6), Hz(NaN), -1, SimTime(800, SIMTIME_NS)) != nullptr); +ASSERT(legacyOfdmModeSet->findMode(Mbps(6), Hz(NaN), -1, SimTime(400, SIMTIME_NS)) == nullptr); + +// Mode-set remapping preserves the complete modeled PHY tuple. In +// particular, a negative GI is an actual absence for compatibility lookup, +// not findMode()'s public wildcard. +const auto erpModeSet = Ieee80211ModeSet::getModeSet("g(erp)"); +const auto pModeSet = Ieee80211ModeSet::getModeSet("p"); +const auto aMode = legacyOfdmModeSet->getMode(Mbps(6), MHz(20), 1, SimTime(800, SIMTIME_NS)); +const auto erpMode = erpModeSet->findCompatibleMode(aMode); +ASSERT(erpMode != nullptr); +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); + +TestIeee80211Transmitter legacyTransmitter; +legacyTransmitter.setModeSet(legacyOfdmModeSet); +legacyTransmitter.setMode(aMode); +legacyTransmitter.setModeSet(erpModeSet); +ASSERT(legacyTransmitter.getSelectedModeSet() == erpModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == erpMode); +const auto modeBeforeRejectedTransition = legacyTransmitter.getSelectedMode(); +bool rejectedIncompatibleModeSet = false; +try { + legacyTransmitter.setModeSet(pModeSet); +} +catch (cRuntimeError&) { + rejectedIncompatibleModeSet = true; +} +ASSERT(rejectedIncompatibleModeSet); +ASSERT(legacyTransmitter.getSelectedModeSet() == erpModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == modeBeforeRejectedTransition); +const auto pMode = pModeSet->getMode(2); // the declared 6 Mbps 10 MHz mode +legacyTransmitter.setModeSetAndMode(pModeSet, pMode); +ASSERT(legacyTransmitter.getSelectedModeSet() == pModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == pMode); + +Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +bool rejectedVhtGreenfieldAfterMixed = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (cRuntimeError&) { + rejectedVhtGreenfieldAfterMixed = true; +} +ASSERT(rejectedVhtGreenfieldAfterMixed); +const auto vhtModeSet = Ieee80211ModeSet::getModeSet("ac"); +const auto vhtLongMcs0 = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +const auto vhtShortMcs0 = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT); +// IEEE Std 802.11-2024, Tables 21-5/21-8 and 21.4.3, Eqs. (21-109)/(21-110). +ASSERT(vhtLongMcs0->getDataMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getSymbolInterval() == SimTime(3600, SIMTIME_NS)); +ASSERT(vhtLongMcs0->getHeaderMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtShortMcs0->getHeaderMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtLongMcs0->getHeaderMode()->getNetBitrate() == vhtShortMcs0->getHeaderMode()->getNetBitrate()); +ASSERT(vhtLongMcs0->getHeaderMode()->getGrossBitrate() == vhtShortMcs0->getHeaderMode()->getGrossBitrate()); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(24)) == SimTime(32400, SIMTIME_NS)); +ASSERT(vhtShortMcs0->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(30)) == SimTime(39600, SIMTIME_NS)); +ASSERT(vhtShortMcs0->getDataDuration(B(30)) == SimTime(40, SIMTIME_US)); +ASSERT(vhtLongMcs0->getDataMode()->getDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtLongMcs0->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +for (auto vhtMode : {vhtLongMcs0, vhtShortMcs0}) + for (auto dataLength : {B(24), B(27), B(30)}) + ASSERT(vhtMode->getDuration(dataLength) == vhtMode->getPreambleDuration() + vhtMode->getHeaderDuration() + vhtMode->getDataDuration(dataLength)); +bool foundVhtLongGuardInterval = false; +bool foundVhtShortGuardInterval = false; +for (int index = 0; index < vhtModeSet->getNumModes(); index++) { + auto vhtMode = dynamic_cast(vhtModeSet->getMode(index)); + if (vhtMode == nullptr) + continue; + auto vhtDataMode = vhtMode->getDataMode(); + auto guardInterval = vhtDataMode->getGuardInterval(); + auto resolvedMode = vhtModeSet->findMode(vhtDataMode->getNetBitrate(), vhtDataMode->getBandwidth(), vhtDataMode->getNumberOfSpatialStreams(), guardInterval); + ASSERT(resolvedMode != nullptr); + ASSERT(resolvedMode->getDataMode()->getGuardInterval() == guardInterval); + foundVhtLongGuardInterval |= guardInterval == SimTime(800, SIMTIME_NS); + foundVhtShortGuardInterval |= guardInterval == SimTime(400, SIMTIME_NS); +} +ASSERT(foundVhtLongGuardInterval); +ASSERT(foundVhtShortGuardInterval); + +auto vhtOneSymbol = dynamic_cast(vhtModeSet->getFastestMode()); +ASSERT(vhtOneSymbol != nullptr); +ASSERT(vhtOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(vhtOneSymbol->getPreambleDuration() == SimTime(20, SIMTIME_US)); +ASSERT(vhtOneSymbol->getHeaderDuration() >= SIMTIME_ZERO); +ASSERT(vhtOneSymbol->getDuration(B(24)) == vhtOneSymbol->getPreambleDuration() + vhtOneSymbol->getHeaderDuration() + vhtOneSymbol->getDataDuration(B(24))); + +for (const auto candidateSet : {modeSet, vhtModeSet}) { + for (int index = 0; index < candidateSet->getNumModes(); index++) { + auto mode = candidateSet->getMode(index); + auto slowerMode = candidateSet->getSlowerMode(mode); + auto fasterMode = candidateSet->getFasterMode(mode); + auto slowerMandatoryMode = candidateSet->getSlowerMandatoryMode(mode); + auto fasterMandatoryMode = candidateSet->getFasterMandatoryMode(mode); + ASSERT(slowerMode == nullptr || slowerMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMode == nullptr || fasterMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); + ASSERT(slowerMandatoryMode == nullptr || slowerMandatoryMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMandatoryMode == nullptr || fasterMandatoryMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); + } +} + +// The equal-rate mandatory candidate is selected by bitrate, not entry index. +auto equalRateVhtMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss2, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +auto mandatoryAtEqualRate = vhtModeSet->getMandatoryModeAtOrBelow(equalRateVhtMode); +ASSERT(mandatoryAtEqualRate != nullptr); +ASSERT(mandatoryAtEqualRate->getDataMode()->getNetBitrate() == equalRateVhtMode->getDataMode()->getNetBitrate()); +ASSERT(vhtModeSet->getIsMandatory(mandatoryAtEqualRate)); + +transmitter.setModeSetAndMode(vhtModeSet, vhtOneSymbol); +ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); +ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); +bool rejectedCombinedModeUpdate = false; +try { + transmitter.setModeSetAndMode(modeSet, greenfieldShortOneSymbol); +} +catch (cRuntimeError&) { + rejectedCombinedModeUpdate = true; +} +ASSERT(rejectedCombinedModeUpdate); +ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); +ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); + +// 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(-1); +rateSelection->finalizeParameters(); +rateSelection->buildInside(); + +TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); +ASSERT(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::applyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection) == nullptr); +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(6.5e6); +rateSelectionHt->finalizeParameters(); +rateSelectionHt->buildInside(); + +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(""); +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(-1); +qosRateSelection->finalizeParameters(); +qosRateSelection->buildInside(); + +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); + +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); + +EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; + +%contains: stdout +HT guard interval catalog, timing, and lookup checks passed. diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 44877fd9d68..7b12d02df28 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" @@ -38,11 +39,73 @@ 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 +{ + 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 +// 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); + 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"); +// 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()); @@ -69,6 +132,20 @@ for (int mcsIndex = 0; mcsIndex < 32; mcsIndex++) { } } } +// 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()); +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); +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/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 { 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 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/Ieee80211OriginatorExpiryReentrant_1.test b/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test new file mode 100644 index 00000000000..1e1d93fc19c --- /dev/null +++ b/tests/unit/Ieee80211OriginatorExpiryReentrant_1.test @@ -0,0 +1,94 @@ +%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 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); + 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 < 4; 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)); + 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)); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); + for (int tid = 0; tid < 4; tid++) + handler.erase(callback.peer, tid); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); + ASSERT(callback.deadline == SIMTIME_MAX); +} diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index b472fd4e291..d3caab216d5 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, - std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false) +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, bool greenfield = 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,17 @@ static Ieee80211Mib::PeerHtState makePeerState(std::initializer_list mcsInd receiverCapabilities.supportedChannelWidths.insert(bandwidth); receiverCapabilities.receiverShortGi20 = shortGi20; receiverCapabilities.receiverShortGi40 = shortGi40; - state.negotiatedCapabilities.operation.operatingChannelWidth = operatingChannelWidth; + receiverCapabilities.receiverGreenfield = greenfield; + 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)"); @@ -77,15 +90,41 @@ 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); -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 +132,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,62 +151,110 @@ 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 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(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()); +// 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 = selectForTest(greenfieldModeSet, &greenfieldPeer, htMixedCtsMode, peer); +ASSERT(greenfieldCtsSelection == htMixedCtsMode); + +bool outsideGreenfieldModeRejected = false; +try { + 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 diff --git a/tests/unit/Ieee80211RecipientExpiryReentrant_1.test b/tests/unit/Ieee80211RecipientExpiryReentrant_1.test new file mode 100644 index 00000000000..516864c26f7 --- /dev/null +++ b/tests/unit/Ieee80211RecipientExpiryReentrant_1.test @@ -0,0 +1,92 @@ +%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 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); + 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)); + 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)); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); + for (int tid = 0; tid < 4; tid++) + handler.erase(callback.peer, tid); + ASSERT(!handler.blockAckAgreementExpired(&callback, &callback)); + ASSERT(callback.deadline == SIMTIME_MAX); +} 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 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/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 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. 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.