From 8540d5fcb02c7a318c399bbc6302177adff8b94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 22 Oct 2026 16:00:58 +0200 Subject: [PATCH 01/16] plan: add: define the S4 correction series and protocol checks The audit requires separate Retry and transfer-completion changes. It also requires protocol evidence and shared arithmetic before production callers. This plan defines six commits and their verification. It preserves the final production source and accepted final fingerprints. The user approves five intermediate network-data values for the isolated Retry commit only. Protocol fixtures belong to the two behavior commits. This plan changes no protocol behavior. Its checks cover links and the existing protection unit. Plan: plan/pending/ieee80211-compressed-blockack-s4-corrections.md Change: plan | behavior.add | - | ieee80211-compressed-blockack-s4-corrections --- ...0211-compressed-blockack-s4-corrections.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 plan/pending/ieee80211-compressed-blockack-s4-corrections.md diff --git a/plan/pending/ieee80211-compressed-blockack-s4-corrections.md b/plan/pending/ieee80211-compressed-blockack-s4-corrections.md new file mode 100644 index 00000000000..cbb38e21837 --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-s4-corrections.md @@ -0,0 +1,67 @@ +# S4 correction series + +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [implementation status](ieee80211-compressed-blockack-replacement.md) + +This series corrects the four findings from the audit of PR #1344. It preserves the final production source at `b716709335f79817542554822279461e61949938`. It adds protocol tests and their explanations. It separates Retry from transfer completion and places shared arithmetic before its callers. + +## Scope and contract + +HCF must set Retry when it retransmits standalone DATA after failed Block Ack feedback. IEEE Std 802.11-2024, 9.2.4.1.6 supplies this requirement. The parent ACK handler already makes that DATA eligible. The correction changes its header before Tx receives it. + +HCF must resolve a final short transfer even without another DATA candidate. IEEE Std 802.11-2024, 10.25.4 requires completion DELBA. Basic remains an INET compatibility profile. This series does not claim complete current-standard Basic support. + +The originator agreement owns cyclic distance, bitmap membership, and window advancement. HCF supplies unresolved identities. The final source already follows this contract. Only its introduction order changes. + +## Proposed commits + +### P0: Record the correction contract + +Add this plan before the implementation commits. The document defines their responsibility, test tasks, and evidence limits. No protocol behavior changes. Protocol tests and a protocol reviewer document do not apply to this documentation commit. Check relative links and series rules. + +### P1: Expose DATA preview without extraction + +Retain the existing preview commit after P0. `IOriginatorQosMacDataService` exposes borrowed preview and prospective sequence lookup. `InProgressFrames` retains extraction as its default. The DATA-selection module fixture checks queue ownership, the unchanged header, and prospective sequence zero. + +This structural commit changes no protocol behavior. New protocol tests and a protocol reviewer document do not apply. Build debug artifacts and run `Ieee80211BlockAckDataSelection_1.test`. + +### P2: Mark DATA retries after failed Block Ack feedback + +The parent can retransmit DATA with Retry clear after a BAR timeout or negative bitmap feedback. HCF already reads ACK status. Move the existing Retry branch into this independent commit. It changes no candidate selection, agreement completion, or reservation policy. + +Add `tests/protocol/wifi/common/WifiBlockAckDataRetry.test`. Start with real association and production ADDBA. Send a continuous UDP stream through the existing infrastructure topology. Disable A-MSDU aggregation. Drop the first Block Ack to sta1 through the radio tap in run 0. Clear its bitmap in run 1. Capture the first Block Ack DATA sequence and TID before BAR. Require retransmission of that same identity with Retry set after BAR. The existing protocol suite has no direct check of this exact path. + +The same fixture must fail before P2 because HCF leaves Retry clear. It must pass after P2 because HCF changes the transmitted header. Add the reviewer document `tests/protocol/wifi/blockack-data-retry.md` with P2. Add a guide and a before/after record under `doc/project/evidence/model/wifi/tests/`. Keep the failure honest; add no expected-failure declaration. + +Build debug artifacts. Run `inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiBlockAckDataRetry\.test$'`. Use seed 0. Check the seven existing changed fingerprint cases to determine P2's exact effects. The user approved five intermediate `~tND` values on 2026-10-22. Adopt those values with P2. Restore the accepted final values with P4. + +### P3: Add shared originator window operations + +Add the final agreement operations before their production callers. Move direct arithmetic assertions into a separate unit fixture. The operations have no production caller until P4. This preparation changes no packet trajectory. New protocol tests and a protocol reviewer document do not apply. Build debug artifacts and run the focused window unit fixture. + +### P4: Complete Basic BAR transfer resolution + +Apply the remaining behavior change through the shared agreement operations. HCF selects BAR before extraction. EDCA permits BAR-only contention. Matched feedback resolves the selected group. Final resolution or expiry starts DELBA. Duration forecasts reserve prepared successors only. + +Add `tests/protocol/wifi/common/WifiBlockAckTransferCompletion.test`. Use real association, production ADDBA, and a finite UDP burst. Run a two-packet burst with an accepted window of 64 and a four-packet burst with a window of one. Keep the BAR threshold at five. Observe ADDBA success, Block Ack DATA, BAR, matched Block Ack, completion DELBA, and its ACK. The empty queue triggers the first case. A full window triggers the second case before further DATA. Existing module fixtures cover these paths, but no protocol fixture checks this complete sequence. + +The fixture must fail before P4 because a short group lacks BAR or completion DELBA. It must pass after P4 through the production owners. Add `tests/protocol/wifi/blockack-transfer-completion.md` with P4. Add its guide and before/after record under `doc/project/evidence/model/wifi/tests/`. Use seed 0. Make no unsupported performance claim. + +Build debug artifacts. Run both new protocol files through explicit filters. Run the seven changed module files and two existing unit files. Compare the seven fingerprint cases with P2 and the pinned final source. Attach each baseline change to its causal behavior commit. Preserve the final approved values. + +The wider protocol selection also checks existing lifecycle fixtures. `WifiBlockAckBarWithoutAgreement.test` must retain its control agreement until the crafted BAR. Hold station contention at 1.002 seconds while queued DATA remains. This prevents completion before the 1.1-second probe without an agreement-state edit. Retain its response assertions. + +The parent passes `WifiBlockAckSameApReassociation.test`, while S4's completion changes its traffic context. Overlap both traffic directions at 6 Mbit/s. Request reassociation at 1.008 seconds, before completion removes the old agreements. Extend fresh traffic until 1.4 seconds. Check fresh roles at 1.38 seconds. Keep the four-role, zero-role, fresh-setup, and UDP-delivery assertions. Disable Block Ack on sta2 and use small downlink packets to isolate the target link's agreements. Permit coincident transmissions as a warning because bidirectional contention can create them. This diagnostic setting changes no reception calculation. + +### P5: Describe the corrected series and evidence + +Update the current implementation documents, commit guide, protocol inventory, and verification record. Give each changed fingerprint row its cause and values. Distinguish fresh execution, source equality, existing evidence, and unavailable approval records. Do not assert an approval whose source is absent. + +This documentation commit changes no protocol behavior. New protocol tests and a protocol reviewer document do not apply. Check links, document structure, series gates, source equality, and the final focused test contract. + +## Verification limits + +The proposed tests observe production MAC exchanges. The radio tap drops feedback only in the Retry test. No fixture supplies an accepted agreement. One fixed seed checks these deterministic paths; it does not establish probabilistic robustness. + +Run each new fixture against its behavior commit and that commit's parent with identical inputs. Keep runtime failure separate from build or setup failure. Render diagrams when a local renderer is available. Report unavailable rendering directly. + +The final source and final existing fingerprint values must equal the pinned source. New tests and documents are the intended final-tree additions. No baseline approval follows from permission to repair source or history. From a01d3a4f063747c1bc8b5e6b8fb95444ba3f37f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 22 Oct 2026 16:00:58 +0200 Subject: [PATCH 02/16] ieee80211: refactor: expose DATA preview without extraction Before this commit, the shared in-progress queue obtains a successor through extraction. Extraction changes ownership and can assign a sequence number. BAR selection needs candidate information before those changes. After this commit, callers can borrow the next eligible frame and query its prospective sequence. Ordinary successor queries retain extraction as their default. Existing transmission behavior remains unchanged. For example, queued DATA retains sequence 77 while preview reports prospective sequence 0. The DATA-selection fixture checks queue ownership, the unchanged header, and the empty in-progress set. This structural change needs no new protocol fixture or protocol reviewer document. Plan: plan/pending/ieee80211-compressed-blockack-s4-corrections.md Change: src.ieee80211 | refactor | test whatsnew migration | ieee80211-compressed-blockack-s4-corrections --- ...80211-basic-block-ack-extension-points.rst | 6 ++++ .../ieee80211-qos-data-preview.txt | 3 ++ .../contract/IOriginatorQosMacDataService.h | 28 +++++++++++++++++++ .../originator/OriginatorQosMacDataService.h | 10 +++---- .../ieee80211/mac/queue/InProgressFrames.cc | 25 +++++++++++++++-- .../ieee80211/mac/queue/InProgressFrames.h | 4 ++- .../Ieee80211BlockAckDataSelection_1.test | 8 ++++++ 7 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst create mode 100644 doc/whatsnew/incompatible/ieee80211-qos-data-preview.txt create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h diff --git a/doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst b/doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst new file mode 100644 index 00000000000..24be3196175 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst @@ -0,0 +1,6 @@ +IEEE 802.11 Basic Block Ack extension points +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +HCF previews the next eligible DATA before BAR selection. Preview must preserve queue ownership and the next sequence number. Custom QoS data services must implement ``IOriginatorQosMacDataService``. This contract extends ``IOriginatorMacDataService`` with ``peekEligibleFrame()``, ``getNextSequenceNumber()``, and ``hasAMsduAggregation()``. + +Custom ``InProgressFrames`` classes must add ``bool extract = true`` to their ``getPendingFrameFor()`` override. When ``extract`` is false, the method must return a borrowed candidate without extraction or sequence assignment. The default value preserves ordinary extraction. Custom candidate sources must also implement ``peekFrameToTransmit()`` without extraction. diff --git a/doc/whatsnew/incompatible/ieee80211-qos-data-preview.txt b/doc/whatsnew/incompatible/ieee80211-qos-data-preview.txt new file mode 100644 index 00000000000..512010e21ac --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-qos-data-preview.txt @@ -0,0 +1,3 @@ +IEEE 802.11 QoS DATA preview contract + +The QoS data service now exposes candidate preview through IOriginatorQosMacDataService. The in-progress queue can preview DATA without extraction or sequence assignment. Ordinary successor queries retain extraction as their default. Custom in-progress queues must accept the extract argument in getPendingFrameFor(). The migration guide describes this interface change. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h new file mode 100644 index 00000000000..e66fa555666 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h @@ -0,0 +1,28 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IORIGINATORQOSMACDATASERVICE_H +#define __INET_IORIGINATORQOSMACDATASERVICE_H + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" + +namespace inet { +namespace ieee80211 { + +class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService +{ + public: + // Preview borrows the frame without extraction or sequence assignment. + virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const = 0; + virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const = 0; + virtual bool hasAMsduAggregation() const = 0; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 7928e2153bc..997a696d46f 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -16,14 +16,14 @@ #include "inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IMsduAggregation.h" #include "inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h" -#include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/contract/ISequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mac/lifetime/EdcaTransmitLifetimeHandler.h" namespace inet { namespace ieee80211 { -class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, public SimpleModule +class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService, public SimpleModule { protected: // Figure 5-1—MAC data plane architecture @@ -58,9 +58,9 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p extractionEligible = eligible; aMsduPermitted = permission; } - Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const; - SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const; - bool hasAMsduAggregation() const { return aMsduAggregationPolicy != nullptr; } + virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const override; + virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const override; + virtual bool hasAMsduAggregation() const override { return aMsduAggregationPolicy != nullptr; } virtual ~OriginatorQosMacDataService(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler, std::function expiration) { lifetimeHandler = handler; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index e9918ee20f7..81ec8985ddc 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" + namespace inet { namespace ieee80211 { @@ -95,9 +97,26 @@ Packet *InProgressFrames::getFrameToTransmit() return nullptr; } -Packet *InProgressFrames::getPendingFrameFor(Packet *frame) +Packet *InProgressFrames::peekFrameToTransmit() +{ + for (auto frame : inProgressFrames) { + if (isEligibleToTransmit(frame)) + return frame; + } + return check_and_cast(dataService)->peekEligibleFrame(pendingQueue); +} + +SequenceNumberCyclic InProgressFrames::getCandidateSequenceNumber(Packet *packet) const +{ + auto header = packet->peekAtFront(); + if (containsFrame(packet)) + return header->getSequenceNumber(); + return check_and_cast(dataService)->getNextSequenceNumber(header->getReceiverAddress(), header->getTid()); +} + +Packet *InProgressFrames::getPendingFrameFor(Packet *frame, bool extract) { - auto frameToTransmit = getFrameToTransmit(); + auto frameToTransmit = extract ? getFrameToTransmit() : peekFrameToTransmit(); if (dynamicPtrCast(frame->peekAtFront())) return frameToTransmit; else { @@ -105,6 +124,8 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) if (isEligibleToTransmit(frame) && frameToTransmit != frame) return frame; } + if (!extract) + return check_and_cast(dataService)->peekEligibleFrame(pendingQueue); auto frames = dataService->extractFramesToTransmit(pendingQueue); if (frames) { auto firstFrame = (*frames)[0]; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 293aebfc3a3..65b13b67aa7 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -53,7 +53,9 @@ class INET_API InProgressFrames : public SimpleModule virtual Packet *getFrames(int i) const { return inProgressFrames[i]; } bool containsFrame(const Packet *packet) const; virtual Packet *getFrameToTransmit(); - virtual Packet *getPendingFrameFor(Packet *frame); + virtual Packet *peekFrameToTransmit(); + SequenceNumberCyclic getCandidateSequenceNumber(Packet *packet) const; + virtual Packet *getPendingFrameFor(Packet *frame, bool extract = true); virtual void dropFrame(Packet *packet); virtual void dropFrames(std::set>> seqAndFragNums); diff --git a/tests/module/Ieee80211BlockAckDataSelection_1.test b/tests/module/Ieee80211BlockAckDataSelection_1.test index 1282837f8ca..93aea4983b5 100644 --- a/tests/module/Ieee80211BlockAckDataSelection_1.test +++ b/tests/module/Ieee80211BlockAckDataSelection_1.test @@ -185,6 +185,14 @@ class DataSelectionTest : public cSimpleModule, public cListener { ASSERT(queue->getPacket(0) == first); ASSERT(service->peekEligibleFrame(queue) == selected); ASSERT(service->getNextSequenceNumber(MacAddress("02:00:00:00:00:02"), 0) == SequenceNumberCyclic(0)); + auto previewService = check_and_cast(service); + auto frames = edca->getEdcaf(AC_BE)->getInProgressFrames(); + auto owner = selected->getOwner(); + ASSERT(previewService->peekEligibleFrame(queue) == selected); + ASSERT(frames->peekFrameToTransmit() == selected); + ASSERT(frames->getCandidateSequenceNumber(selected) == SequenceNumberCyclic(0)); + ASSERT(frames->getLength() == 0 && selected->getOwner() == owner); + ASSERT(selected->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); wait(SimTime(100, SIMTIME_MS)); auto extracted = service->extractFramesToTransmit(queue); ASSERT(extracted != nullptr && extracted->size() == 1); From fb01a82fd0741ca2ceda967e7ad8c6571510f47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 22 Oct 2026 16:00:58 +0200 Subject: [PATCH 03/16] ieee80211: fix: mark DATA retries after failed Block Ack feedback Before this commit, lost or negative Block Ack feedback makes DATA eligible for retransmission. HCF can send that DATA with Retry clear because its header does not reflect the failed feedback state. After this commit, HCF sets Retry before Tx receives the retransmitted DATA. The sequence and TID remain unchanged. IEEE Std 802.11-2024, 9.2.4.1.6 requires Retry for this retransmission. Candidate selection and completion stay intact. WifiBlockAckDataRetry.test uses real association and ADDBA. Run 0 drops the first response. Run 1 clears its bitmap. Both require Retry on retransmission of the captured identity. Both fail on the parent and pass with this branch. tests/protocol/wifi/blockack-data-retry.md explains the protocol evidence. The user approved five intermediate ~tND values on 2026-10-22. Retry and its computed FCS change network-data content in these run-0 configurations: - Block Ack NoFragmentation: 2cd4-4795 -> 0f38-6042. - Block Ack Fragmentation: feff-7e88 -> ddec-9e3d. - Block Ack MixedTraffic: 9724-d1ce -> 2839-8753. - Fragmentation HCFfragblockack: c474-7174 -> bbe5-0771. - Wireless MacQosWithBlockAck: 538e-67df -> 1914-cd4d. All seven event and packet-length fingerprints remain unchanged. TXOP General and ad hoc MacQos run 1 retain all selected values. The seven-case check passes. The later completion commit restores the accepted final S4 rows. Plan: plan/pending/ieee80211-compressed-blockack-s4-corrections.md Change: src.ieee80211.Hcf | behavior.change.fix | test whatsnew fingerprint | ieee80211-compressed-blockack-s4-corrections --- .../tests/blockack-data-retry-comparison.md | 44 ++++++++++ .../model/wifi/tests/blockack-data-retry.md | 52 +++++++++++ .../fixes/ieee80211-block-ack-data-retry.txt | 3 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 11 +++ tests/fingerprint/examples.csv | 2 +- tests/fingerprint/showcases.csv | 8 +- tests/protocol/wifi/README.md | 9 +- tests/protocol/wifi/blockack-data-retry.md | 31 +++++++ .../wifi/common/WifiBlockAckDataRetry.test | 87 +++++++++++++++++++ 9 files changed, 238 insertions(+), 9 deletions(-) create mode 100644 doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md create mode 100644 doc/project/evidence/model/wifi/tests/blockack-data-retry.md create mode 100644 doc/whatsnew/fixes/ieee80211-block-ack-data-retry.txt create mode 100644 tests/protocol/wifi/blockack-data-retry.md create mode 100644 tests/protocol/wifi/common/WifiBlockAckDataRetry.test diff --git a/doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md b/doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md new file mode 100644 index 00000000000..faab29d07a1 --- /dev/null +++ b/doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md @@ -0,0 +1,44 @@ +# DATA Retry: before and after the correction + +> **Kind:** measurement · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [guide](blockack-data-retry.md) + +Both feedback cases fail at the Retry assertion on the parent. Both pass when the control adds only HCF's Retry branch. The final S4 source also passes both cases. + +## Exact inputs and source controls + +| Control | Production source | +| --- | --- | +| Parent | Commit `02f24a3a4d519511cf24731e25d926068b11d24d`; source tree `2a26acc1bd1d8d5ccac42c509cbfced219554175`. | +| Isolated correction | The same parent plus HCF's eleven-line Retry branch; source tree `5a58b7601444e9872312112e6f6e8ffc86207f67`. | +| Integrated S4 | Source tree `20aade533739a1877a87391909da7dd2a91c79c7`. | + +All controls use the same `WifiBlockAckDataRetry.test` input. Its SHA-256 is `271696667cf84e675683c6e146d66fd0bd57fed144cf3221caefd7e18e2c14bf`. The fixture and shared lifecycle support build on both sides. No expected-failure declaration applies. + +## Observed results + +| Run | Parent | Isolated correction | Integrated S4 | +| --- | --- | --- | --- | +| 0: lost Block Ack | FAIL at the Retry predicate. | PASS. | PASS. | +| 1: cleared bitmap | FAIL at the Retry predicate. | PASS. | PASS. | + +The parent reaches the retransmission of the captured sequence and TID. It fails because Retry remains clear. These failures are runtime assertion results, not setup or compilation errors. The isolated source changes only that transmitted header branch. + +## Commands and artifacts + +Build each source with `make MODE=debug -j12` before the test. From each checkout root, run: + +```sh +inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiBlockAckDataRetry\.test$' +``` + +The parent run uses `/tmp/pr1344-controls-20261022`. Its combined control command also selects the completion and two existing lifecycle fixtures. The parent log is `/tmp/pr1344-control-base-protocol.log`. Preserved outputs are under `/tmp/pr1344-parent-protocol-artifacts/WifiBlockAckDataRetry/`. + +The isolated run uses the same retained worktree after the source patch. Its log is `/tmp/pr1344-control-retry-protocol.log`. The integrated log is `/tmp/pr1344-correction-final-protocol-regression.log`. Both integrated Retry cases pass. The combined parent runner returns 1 for the two new failing fixtures. + +All runs use General configuration, runs 0 and 1, seed 0, and a 1.1-second limit. The protocol program stops each run when it reaches its verdict. No additional seed or loss campaign follows from these results. + +## Fingerprint scope + +The isolated correction completes seven existing fingerprint cases. Five `~tND` values differ. Every selected event and packet-length fingerprint remains equal to its parent baseline. The detailed calculation is `/tmp/pr1344-retry-fingerprints.out`. + +The user approved the five intermediate values on 2026-10-22. They apply only before transfer completion enters the series. The final S4 source retains its seven previously accepted baseline rows. These measurements do not establish aggregate retransmission behavior or complete Basic conformance. diff --git a/doc/project/evidence/model/wifi/tests/blockack-data-retry.md b/doc/project/evidence/model/wifi/tests/blockack-data-retry.md new file mode 100644 index 00000000000..343f1134bc9 --- /dev/null +++ b/doc/project/evidence/model/wifi/tests/blockack-data-retry.md @@ -0,0 +1,52 @@ +# Standalone DATA Retry after Block Ack failure + +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [test](../../../../../../tests/protocol/wifi/common/WifiBlockAckDataRetry.test), [comparison](blockack-data-retry-comparison.md) + +The originator must set Retry when it retransmits standalone DATA after failed Block Ack feedback. The test observes the production transmitted header. It requires the retransmission to retain the captured sequence and TID. + +## Scenario and requirement + +The infrastructure network contains an AP and two stations. A UDP stream starts at sta1 after association. The AP relays its traffic to sta2. Production ADDBA establishes the uplink agreement. Seed 0 and a 0.1 ms send interval make the selected exchange deterministic. + +The test selects INET's Basic compatibility profile and disables A-MSDU aggregation. It inherits the lifecycle configuration and its inactive inactivity timer. The continuous stream reaches threshold five without a final-transfer requirement. + +IEEE Std 802.11-2024, 9.2.4.1.6 requires Retry for retransmitted standalone DATA. Lost feedback can occur between compliant peers. The cleared bitmap is a controlled negative-feedback input. It tests the originator's response; it does not model the physical cause of every failed DATA reception. + +## Faults and assertions + +| Run | Fault at sta1 | Required result | +| --- | --- | --- | +| 0 | The radio tap drops the first Block Ack. | The same DATA sequence and TID return with Retry set after timeout. | +| 1 | The radio tap clears all bits in the first Block Ack. | The same DATA identity returns with Retry set after negative feedback. | + +The first observed Block Ack DATA must have Retry clear. The test captures its identity before BAR. It next observes BAR, then the retransmission of that identity. Typed header queries avoid an ambiguous field lookup through the packet trailer. + +The radio tap changes only received feedback. Association, agreement setup, DATA selection, timeout, ACK status, and retransmission remain production operations. + +```mermaid +sequenceDiagram + participant O as Originator + participant R as Recipient + O->>R: Standalone DATA, Retry = 0 + O->>R: BAR + alt Run 0: feedback loss + R--xO: Block Ack lost at the radio tap + else Run 1: negative feedback + R-->>O: Block Ack with cleared bitmap + end + alt Before P2 + O->>R: Same sequence and TID, Retry = 0 + else After P2 + O->>R: Same sequence and TID, Retry = 1 + end +``` + +## Cause and correction + +The parent ACK handler already makes missing-feedback and negative-feedback DATA eligible. HCF previously forwarded that frame without setting Retry. The independent correction changes the header before Tx receives the retransmission. No candidate preview or completion DELBA is necessary for this path. + +The [reviewer document](../../../../../../tests/protocol/wifi/blockack-data-retry.md) associates this fixture with plan P2. The [comparison record](blockack-data-retry-comparison.md) separates executed evidence from expected behavior. + +## Limits + +Two fixed-seed cases check header identity and Retry. They do not estimate loss probability, cover aggregates, or establish complete Basic conformance. The tap's cleared bitmap is test stimulus. It is not evidence of a particular physical error pattern. diff --git a/doc/whatsnew/fixes/ieee80211-block-ack-data-retry.txt b/doc/whatsnew/fixes/ieee80211-block-ack-data-retry.txt new file mode 100644 index 00000000000..2b9e15600c4 --- /dev/null +++ b/doc/whatsnew/fixes/ieee80211-block-ack-data-retry.txt @@ -0,0 +1,3 @@ +IEEE 802.11 DATA Retry after Block Ack failure + +HCF now sets Retry when it retransmits standalone QoS DATA after lost or negative Block Ack feedback. The retransmission retains its sequence number and TID. Configurations with these retransmissions can produce different network-data fingerprints, including computed FCS values. The correction changes no packet length or transmission schedule by itself. diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index f5b07949b67..1bbafa246de 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -1623,6 +1623,17 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) return false; if (!isFrameEligible(packet, true)) return false; + if (auto dataHeader = dynamicPtrCast(header)) { + auto status = channelOwner->getAckHandler()->getQoSDataAckStatus(dataHeader); + if (dataHeader->getType() == ST_DATA_WITH_QOS && + (status == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED || status == QosAckHandler::Status::BLOCK_ACK_ARRIVED_UNACKED)) { + // IEEE Std 802.11-2024, 9.2.4.1.6: DATA after a failed Block Ack exchange is a retransmission. + auto updated = packet->removeAtFront(); + updated->setRetry(true); + packet->insertAtFront(updated); + header = updated; + } + } auto lifetimeHandler = edca->getLifetimeHandler(); auto step = frameSequenceHandler->getContext()->getLastStep(); auto protectedPacket = dynamic_cast(step); diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 78905809d2c..e4a86e712bd 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -658,7 +658,7 @@ /examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, d929-5913/tplx;f602-4424/~tNl;f2fb-4d98/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 0473-f9d2/tplx;e92c-7352/~tNl;317d-aa4f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, e07a-4c5f/tplx;9f6c-7889/~tNl;29ea-169c/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, f8a4-9474/tplx;1cba-cccc/~tNl;538e-67df/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, f8a4-9474/tplx;1cba-cccc/~tNl;1914-cd4d/~tND;08b5-d005/tyf, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 836d42b5183..9416b34980b 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -199,9 +199,9 @@ /showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;7b7c-2bfb/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;d680-b8ad/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, b828-6bd6/tplx;026f-2c6d/~tNl;2cd4-4795/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 021a-d29a/tplx;1597-ec6a/~tNl;feff-7e88/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 5b9e-801f/tplx;8731-2504/~tNl;9724-d1ce/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, b828-6bd6/tplx;026f-2c6d/~tNl;0f38-6042/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 021a-d29a/tplx;1597-ec6a/~tNl;ddec-9e3d/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 5b9e-801f/tplx;8731-2504/~tNl;2839-8753/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;fc60-7eb7/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;6f3a-074a/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -267,7 +267,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 8df2-40a3/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, c99f-354d/tplx;8e10-a3f0/~tNl;ac4b-3ba1/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, f509-65f1/tplx;6b3a-0d65/~tNl;c474-7174/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, f509-65f1/tplx;6b3a-0d65/~tNl;bbe5-0771/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 6150-7a8b/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless diff --git a/tests/protocol/wifi/README.md b/tests/protocol/wifi/README.md index 04aa9be0fee..5d5798887a9 100644 --- a/tests/protocol/wifi/README.md +++ b/tests/protocol/wifi/README.md @@ -36,24 +36,25 @@ Each protocol test has two documents. The guide explains its scenario, assertion | `WifiBlockAckBarWithoutAgreement.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement-comparison.md) | | `WifiBlockAckInactivity.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-inactivity.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-inactivity-comparison.md) | | `WifiBlockAckSameApReassociation.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation-comparison.md) | +| `WifiBlockAckDataRetry.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-data-retry.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md) | The [shared procedure](../../../doc/project/evidence/model/wifi/pr1343-comparison-method.md) explains how to reproduce each comparison with identical fixture inputs. ## Source inventory -The suite contains **80 tests: 50 default PASS expectations and 30 declared FAIL expectations**. +The suite contains **81 tests: 51 default PASS expectations and 30 declared FAIL expectations**. These are source declarations, not a fresh full-suite result. | Directory | Tests | Expected PASS | Expected FAIL | | --- | ---: | ---: | ---: | -| `common/` | 24 | 15 | 9 | +| `common/` | 25 | 16 | 9 | | `legacy/` | 6 | 5 | 1 | | `11b/` | 6 | 4 | 2 | | `11a/` | 4 | 4 | 0 | | `11g/` | 7 | 6 | 1 | | `11n/` | 18 | 11 | 7 | | `11ac/` | 15 | 5 | 10 | -| **Total** | **80** | **50** | **30** | +| **Total** | **81** | **51** | **30** | The [audit report](../../../doc/project/evidence/model/wifi/results.md) owns the observed results, failure classifications, repair dependencies, and verification records. It includes @@ -61,7 +62,7 @@ six repaired behavior checks: HT Capabilities, HT Operation, the modeled dummy f authentication exchange, deauthentication, reassociation, and deterministic retransmission. The authentication check does not establish WEP cryptography. -The five comparison records isolate each test at its related fix and that fix's parent. The BAR and inactivity records use the same lifecycle commit. Each record identifies its inputs, observations, controls, and executed source revisions. These Basic compatibility tests do not establish full standards conformance. +The comparison records isolate each test at its related fix and that fix's parent. The BAR and inactivity records use the same lifecycle commit. Each record identifies its inputs, observations, controls, and executed source revisions. These Basic compatibility tests do not establish full standards conformance. Of the 30 expected failures, 17 are retained unsupported-feature probes or explicit limitations; 13 are deferred repairs declared by maintainer decision. Five of those 13 retain explicit diff --git a/tests/protocol/wifi/blockack-data-retry.md b/tests/protocol/wifi/blockack-data-retry.md new file mode 100644 index 00000000000..6159d28776b --- /dev/null +++ b/tests/protocol/wifi/blockack-data-retry.md @@ -0,0 +1,31 @@ +# DATA Retry after failed Block Ack feedback + +Plan P2, **Mark DATA retries after failed Block Ack feedback**, changes HCF's transmitted header. The test is [WifiBlockAckDataRetry.test](common/WifiBlockAckDataRetry.test). + +Before this commit, HCF can retransmit standalone DATA after failed feedback with Retry clear. The ACK handler already retains the sequence and TID and makes the DATA eligible. The failure therefore concerns the transmitted header, not retransmission eligibility. + +After this commit, HCF sets Retry when the ACK status reports missing feedback or negative feedback. IEEE Std 802.11-2024, 9.2.4.1.6 defines this field for retransmitted standalone DATA. + +The fixture uses real association and production ADDBA. It captures the first Block Ack DATA identity. Run 0 drops the first Block Ack at the originator. Run 1 clears that response's bitmap. Both runs require the same DATA identity after BAR, with Retry set. A radio tap supplies the fault; it does not create an agreement or change originator state. + +```mermaid +sequenceDiagram + participant O as Originator + participant R as Recipient + O->>R: Standalone DATA, Retry = 0 + O->>R: BAR + alt Run 0: feedback loss + R--xO: Block Ack lost at the radio tap + else Run 1: negative feedback + R-->>O: Block Ack with cleared bitmap + end + alt Before P2 + O->>R: Same sequence and TID, Retry = 0 + else After P2 + O->>R: Same sequence and TID, Retry = 1 + end +``` + +The [guide](../../../doc/project/evidence/model/wifi/tests/blockack-data-retry.md) defines setup and assertions. The [comparison](../../../doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md) records executed results and limits. + +This test checks the standalone Basic compatibility path. It proves neither A-MPDU retry behavior nor complete IEEE 802.11-2024 Basic conformance. diff --git a/tests/protocol/wifi/common/WifiBlockAckDataRetry.test b/tests/protocol/wifi/common/WifiBlockAckDataRetry.test new file mode 100644 index 00000000000..735436129ca --- /dev/null +++ b/tests/protocol/wifi/common/WifiBlockAckDataRetry.test @@ -0,0 +1,87 @@ +%description: +Production DATA retransmission after one lost Block Ack must retain its sequence and TID with Retry set. +The radio tap drops the first Block Ack or clears its bitmap at sta1. +Real association and ADDBA precede the DATA exchange. +IEEE Std 802.11-2024, 9.2.4.1.6 defines Retry for retransmitted standalone DATA. + +%extraargs: -r 0..1 + +%file: BlockAckDataRetry.cc +#include "BlockAckLifecycleTestSupport.h" + +namespace inet::protocoltest { +static void clearBitmap(Packet *packet) +{ + std::vector> prefix; + while (!dynamicPtrCast(packet->peekAtFront())) + prefix.push_back(packet->removeAtFront(packet->peekAtFront()->getChunkLength())); + auto response = packet->removeAtFront(); + for (int i = 0; i < 64; i++) { + BitVector bits; + for (int fragment = 0; fragment < 16; fragment++) + bits.appendBit(false); + response->setBlockAckBitmap(i, bits); + } + packet->insertAtFront(response); + for (auto it = prefix.rbegin(); it != prefix.rend(); ++it) + packet->insertAtFront(*it); +} + +static ProtocolTest retryProgram(const char *name, bool negative) +{ + auto test = ProtocolTest(name); + if (negative) + test.intercept(tap("radioMedium").to("sta1").filterPacket("ieee80211mac.type == 25").nth(1).mutate(clearBitmap) + .describe("clear the first feedback bitmap at the originator")); + else + test.intercept(tap("radioMedium").to("sta1").filterPacket("ieee80211mac.type == 25").nth(1).drop() + .describe("lose the first Block Ack at the originator")); + return test.once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + auto data = dynamicPtrCast(c.event.packet->peekAtFront()); + return data && data->getAckPolicy() == BLOCK_ACK; + }) + .capture("sequence", [](const PacketEvent& event) { + return cValue(event.packet->peekAtFront()->getSequenceNumber().get()); + }) + .capture("tid", [](const PacketEvent& event) { + return cValue(event.packet->peekAtFront()->getTid()); + }) + .assertEvent([](const MatchContext& c) { + return !c.event.packet->peekAtFront()->getRetry(); + }) + .describe("first standalone DATA under the accepted agreement").within(1.05)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 24") + .describe("BAR requests feedback for the outstanding DATA").within(0.02)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + auto data = dynamicPtrCast(c.event.packet->peekAtFront()); + return data && data->getType() == ST_DATA_WITH_QOS && + data->getSequenceNumber().get() == c.get("sequence").intValue() && data->getTid() == c.get("tid").intValue(); + }) + .assertEvent([](const MatchContext& c) { + return c.event.packet->peekAtFront()->getRetry(); + }) + .describe("the same DATA identity returns with Retry set").within(0.02)); +} + +Define_ProtocolTest(wifi_block_ack_data_retry) { return retryProgram("wifi_block_ack_data_retry", false); } +Define_ProtocolTest(wifi_block_ack_data_retry_negative) { return retryProgram("wifi_block_ack_data_retry_negative", true); } +} + +%inifile: test.ini +[General] +cmdenv-stop-batch-on-error = false +*.tester.testName = ${negative=false,true} ? "wifi_block_ack_data_retry_negative" : "wifi_block_ack_data_retry" +*.radioMedium.typename = "Ieee80211ScalarTapRadioMedium" +*.sta1.app[0].sendInterval = 0.1ms +*.sta1.app[0].stopTime = 1.02s +**.originatorMacDataService.msduAggregationPolicy.typename = "" +sim-time-limit = 1.1s +include ../../ini/_blockackLifecycle.ini + +%contains: stdout +PROTOCOLTEST wifi_block_ack_data_retry: PASS +%contains: stdout +PROTOCOLTEST wifi_block_ack_data_retry_negative: PASS From 5e17e6ffedf3198a73e448257980b45a66bc4a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 22 Oct 2026 16:00:58 +0200 Subject: [PATCH 04/16] ieee80211: refactor: add shared originator window operations Before this commit, the agreement stores its window start but lacks the shared operations for cyclic distance, bitmap membership, and window advancement. The next commit needs these operations in HCF and both ACK owners. After this commit, OriginatorBlockAckAgreement owns these operations. No production caller uses them yet. Packet trajectories remain unchanged. For example, a four-position window at 4094 contains 4094, 4095, 0, and 1. Unresolved sequence 4095 prevents advancement to 0. The new unit fixture checks that boundary and the separate 64-position response bitmap. The Retry protocol fixture remains green. This preparation needs no new protocol fixture or protocol reviewer document because it changes no transmitted behavior. Plan: plan/pending/ieee80211-compressed-blockack-s4-corrections.md Change: src.ieee80211.OriginatorBlockAckAgreement | refactor | test | ieee80211-compressed-blockack-s4-corrections --- .../blockack/OriginatorBlockAckAgreement.h | 25 ++++++++++++++++ .../Ieee80211OriginatorBlockAckWindow_1.test | 30 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/unit/Ieee80211OriginatorBlockAckWindow_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index 8ef73652db3..c760205173a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -8,6 +8,8 @@ #ifndef __INET_ORIGINATORBLOCKACKAGREEMENT_H #define __INET_ORIGINATORBLOCKACKAGREEMENT_H +#include + #include "inet/linklayer/ieee80211/mac/common/SequenceControlField.h" namespace inet { @@ -47,6 +49,29 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual ~OriginatorBlockAckAgreement() {} virtual int getBufferSize() const { return bufferSize; } + int getWindowSize() const { return std::min(bufferSize, 64); } + static int computeSequenceDistance(SequenceNumberCyclic start, SequenceNumberCyclic sequence) + { + return (sequence.get() - start.get() + 4096) % 4096; + } + bool containsSequence(SequenceNumberCyclic sequence) const + { + return computeSequenceDistance(startingSequenceNumber, sequence) < getWindowSize(); + } + // The Basic and Compressed bitmaps cover 64 sequence positions, independent of the accepted window size. + static bool isInBitmap(SequenceNumberCyclic sequence, SequenceNumberCyclic bitmapStart) + { + return computeSequenceDistance(bitmapStart, sequence) < 64; + } + // Preserve the earliest unresolved position, or advance to the next allocation when none remains. + void advanceWindow(SequenceNumberCyclic nextSequence, const std::vector& unresolvedSequences) + { + auto start = getStartingSequenceNumber(); + int distance = computeSequenceDistance(start, nextSequence); + for (auto sequence : unresolvedSequences) + distance = std::min(distance, computeSequenceDistance(start, sequence)); + setStartingSequenceNumber(start + distance); + } virtual SequenceNumberCyclic getStartingSequenceNumber() { return startingSequenceNumber; } virtual void setStartingSequenceNumber(SequenceNumberCyclic sequenceNumber) { startingSequenceNumber = sequenceNumber; } virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; } diff --git a/tests/unit/Ieee80211OriginatorBlockAckWindow_1.test b/tests/unit/Ieee80211OriginatorBlockAckWindow_1.test new file mode 100644 index 00000000000..5526e1cf823 --- /dev/null +++ b/tests/unit/Ieee80211OriginatorBlockAckWindow_1.test @@ -0,0 +1,30 @@ +%description: +Shared originator operations preserve the earliest unresolved sequence across wraparound. +The accepted transmit window and the 64-position response bitmap have distinct boundaries. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +using namespace inet; +using namespace inet::ieee80211; + +%activity: +const MacAddress peer("02:00:00:00:00:02"); +OriginatorBlockAckAgreement window(peer, 5, SequenceNumberCyclic(4094), 4, false, false); +ASSERT(window.containsSequence(SequenceNumberCyclic(4094))); +ASSERT(window.containsSequence(SequenceNumberCyclic(1))); +ASSERT(!window.containsSequence(SequenceNumberCyclic(2))); +ASSERT(!window.containsSequence(SequenceNumberCyclic(4093))); +window.advanceWindow(SequenceNumberCyclic(2), {SequenceNumberCyclic(0), SequenceNumberCyclic(4095)}); +ASSERT(window.getStartingSequenceNumber() == SequenceNumberCyclic(4095)); +window.advanceWindow(SequenceNumberCyclic(2), {SequenceNumberCyclic(0)}); +ASSERT(window.getStartingSequenceNumber() == SequenceNumberCyclic(0)); +window.advanceWindow(SequenceNumberCyclic(2), {}); +ASSERT(window.getStartingSequenceNumber() == SequenceNumberCyclic(2)); +ASSERT(OriginatorBlockAckAgreement::isInBitmap(SequenceNumberCyclic(62), SequenceNumberCyclic(4095))); +ASSERT(!OriginatorBlockAckAgreement::isInBitmap(SequenceNumberCyclic(63), SequenceNumberCyclic(4095))); +ASSERT(!OriginatorBlockAckAgreement::isInBitmap(SequenceNumberCyclic(4094), SequenceNumberCyclic(4095))); +ASSERT(!window.containsSequence(SequenceNumberCyclic(62))); +std::cout << "Originator Block Ack window assertions passed.\n"; + +%contains: stdout +Originator Block Ack window assertions passed. From fad2f7dd155d8d014996e0b5081a840b2ab0ba9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 22 Oct 2026 16:03:50 +0200 Subject: [PATCH 05/16] ieee80211: change: complete Basic BAR transfer resolution Before this commit, a short Basic transfer can lack final feedback below the BAR threshold. Channel access requires another DATA candidate. A full window or an empty queue can therefore block completion. After this commit, HCF selects BAR before DATA extraction and permits BAR-only contention. The ACK policy separates outstanding DATA by peer and TID within the accepted window. Matched feedback resolves only that group. Shared agreement operations advance the window without duplicate cyclic arithmetic. Final resolution or expiry queues DELBA at a safe exchange boundary. BAR failure changes contention state without a DATA retry charge. Basic single protection reserves only prepared successors. Unextracted DATA starts another TXOP. These actions share one candidate contract without extraction. They stay together to preserve that contract across selection, reservation, and continuation. For example, a one-position window with threshold five needs BAR before the next sequence can proceed. WifiBlockAckTransferCompletion.test checks that case and a two-packet burst. Both fail on the parent and pass through BAR, matched feedback, DELBA, and ACK after this commit. The protocol explanation is tests/protocol/wifi/blockack-transfer-completion.md. The output checks reject any FAIL verdict, so one successful run cannot hide another run's failure. The existing BAR control holds contention with queued DATA to retain its agreement. The reassociation fixture probes before completion and extends fresh traffic. Both retain their state and response assertions. Seven module files and three unit files cover feedback, expiry, collision, and timing. The seven final fingerprint rows retain the user's 2026-10-15 acceptance. This commit replaces the five approved intermediate Retry values. Each tuple below names the resulting non-graphical ingredients for its configuration. - Block Ack NoFragmentation run 0: 9161-625d/tplx;6a02-09aa/~tNl;cbc0-3b0a/~tND. Final Normal Ack DATA reserves its ACK. Setup and BAR order also change. - Block Ack Fragmentation run 0: 36f4-5c22/tplx;66b0-94cd/~tNl;45f3-af4f/~tND. Final Block Ack DATA reserves zero. BAR obtains a separate TXOP. - Block Ack MixedTraffic run 0: 351b-d4ba/tplx;5c1f-69a8/~tNl;6182-6da3/~tND. The reservation excludes an unextracted large successor. Setup can precede it. - Fragmentation HCFfragblockack run 0: 2597-1e2e/tplx;d269-c6ae/~tNl;2f5c-4cca/~tND. Retry already comes from the parent. The final burst boundary now changes. - TXOP General run 0: ab91-87f3/tplx;f30c-6aa4/~tNl. HCF excludes an unextracted successor and ends the sequence at that boundary. - Ad hoc MacQos run 1: 4fd2-bb56/tplx;ecaf-89f7/~tNl;9398-0fae/~tND. Final Normal Ack DATA reserves SIFS plus ACK. Further extraction needs a TXOP. - Wireless MacQosWithBlockAck run 0: 6edb-0326/tplx;2a8a-50ee/~tNl;4301-882d/~tND. ADDBA reserves its ACK without unrelated DATA. Later extraction changes aggregation. The final CSV files and production source equal the original PR head. Graphical values remain unchanged and unverified. Basic stays the default compatibility profile. Compressed exchanges, general Basic DATA admission, and complete NAV conformance remain outside this change. Plan: plan/pending/ieee80211-compressed-blockack-s4-corrections.md Change: src.ieee80211 | behavior.change | test whatsnew migration fingerprint | ieee80211-compressed-blockack-s4-corrections --- .../tests/blockack-bar-without-agreement.md | 2 +- .../tests/blockack-same-ap-reassociation.md | 12 +- ...blockack-transfer-completion-comparison.md | 59 +++ .../tests/blockack-transfer-completion.md | 53 ++ ...80211-basic-block-ack-extension-points.rst | 8 + ...11-basic-block-ack-transfer-completion.txt | 9 + .../OriginatorBlockAckAgreementHandler.cc | 1 - .../ieee80211/mac/channelaccess/Edca.h | 3 +- .../mac/contract/IOriginatorQoSAckPolicy.h | 5 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 100 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 2 + .../ieee80211/mac/framesequence/HcfFs.cc | 27 +- .../framesequence/PrimitiveFrameSequences.cc | 10 +- .../ieee80211/mac/framesequence/TxOpFs.cc | 5 +- .../mac/originator/OriginatorQosAckPolicy.cc | 97 ++-- .../mac/originator/OriginatorQosAckPolicy.h | 7 +- .../ieee80211/mac/originator/QosAckHandler.cc | 18 +- .../ieee80211/mac/originator/QosAckHandler.h | 3 +- .../mac/originator/QosRecoveryProcedure.cc | 14 + .../mac/originator/QosRecoveryProcedure.h | 1 + .../ieee80211/mac/queue/InProgressFrames.cc | 3 +- tests/fingerprint/examples.csv | 4 +- tests/fingerprint/showcases.csv | 10 +- .../Ieee80211BlockAckCompletionCost_1.test | 210 ++++++++ tests/module/Ieee80211BlockAckDeferral_1.test | 34 +- ...Ieee80211BlockAckFragmentRetirement_1.test | 57 ++- .../module/Ieee80211BlockAckScheduling_1.test | 455 ++++++++++++++++++ tests/module/Ieee80211HcfHeader_1.test | 2 +- .../Ieee80211HcfMultipleProtection_2.test | 11 +- tests/protocol/wifi/README.md | 7 +- .../wifi/blockack-transfer-completion.md | 32 ++ .../WifiBlockAckBarWithoutAgreement.test | 13 +- .../WifiBlockAckSameApReassociation.test | 27 +- .../WifiBlockAckTransferCompletion.test | 76 +++ tests/unit/Ieee80211BlockAckScheduling_1.test | 153 ++++++ tests/unit/Ieee80211MultipleProtection_1.test | 3 +- 36 files changed, 1404 insertions(+), 129 deletions(-) create mode 100644 doc/project/evidence/model/wifi/tests/blockack-transfer-completion-comparison.md create mode 100644 doc/project/evidence/model/wifi/tests/blockack-transfer-completion.md create mode 100644 doc/whatsnew/incompatible/ieee80211-basic-block-ack-transfer-completion.txt create mode 100644 tests/module/Ieee80211BlockAckCompletionCost_1.test create mode 100644 tests/module/Ieee80211BlockAckScheduling_1.test create mode 100644 tests/protocol/wifi/blockack-transfer-completion.md create mode 100644 tests/protocol/wifi/common/WifiBlockAckTransferCompletion.test create mode 100644 tests/unit/Ieee80211BlockAckScheduling_1.test diff --git a/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement.md b/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement.md index 29a02dff021..f3da37311b8 100644 --- a/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement.md +++ b/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement.md @@ -43,7 +43,7 @@ Both runs use real association, production transmission, and the wireless medium | 0 | No agreement for station `sta1` and TID 5 | Recipient-role DELBA for that station and TID | | 1 | Real ADDBA establishes an agreement for `sta1` and TID 6 | Basic Block Ack for that station and TID | -Run 0 starts application traffic after the simulation's time limit. Run 1 supplies a short traffic batch before the crafted BAR. Read-only queries check the required agreement state at both endpoints before transmission. The control must establish its agreement before the response assertion can test normal behavior. +Run 0 starts application traffic after the simulation's time limit. Run 1 supplies a short traffic batch before the crafted BAR. At 1.002 seconds, the fixture blocks station contention while queued DATA remains. This control retains the agreement until the crafted BAR, despite S4's completion teardown. It changes no agreement state. Read-only queries check the required agreement state at both endpoints before transmission. ```mermaid sequenceDiagram diff --git a/doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation.md b/doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation.md index 12b4a8b815d..a02457f892d 100644 --- a/doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation.md +++ b/doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation.md @@ -40,7 +40,9 @@ The fixture creates these roles through real ADDBA exchanges. It never inserts a ## Test sequence -The initial uplink batch starts at `1 s`. The reverse batch starts at `1.01 s`. Both batches end before replacement. At `1.1 s`, the station agent requires all four roles and saves the peer rate sets and advertised HT capabilities at both endpoints. +The initial uplink batch starts at `1 s`. The reverse batch starts one microsecond later. Both batches end before replacement. At `1.008 s`, the station agent requires all four roles and saves the peer rate sets and advertised HT capabilities at both endpoints. This early probe precedes S4's completion teardown. + +The fixture uses 6 Mbit/s and disables A-MSDU aggregation. It disables Block Ack on sta2 and uses 100-byte downlink packets. The AP's Block Ack length limit excludes its larger forwarded uplink packets. These controls isolate agreements on the target link. Coincident transmissions produce a warning; this diagnostic setting changes no reception calculation. The agent requests reassociation with its current AP without another authentication exchange. This condition matters because authentication could clear the old association before the test reaches the replacement path. The request's receiver and Current AP field must both identify the current AP. @@ -50,7 +52,7 @@ sequenceDiagram participant G as Station test agent participant A as AP management and HCF Note over S,A: Real traffic establishes both agreement directions - G->>G: Require four roles and save peer parameters at 1.1 s + G->>G: Require four roles and save peer parameters at 1.008 s G->>S: Request reassociation with current AP S->>A: Reassociation Request A-->>S: Successful Reassociation Response @@ -61,7 +63,7 @@ sequenceDiagram Note over S,A: New traffic starts at 1.3 s and 1.31 s S->>A: Fresh uplink ADDBA setup A->>S: Fresh downlink ADDBA setup - G->>G: Require four fresh roles at 1.4 s + G->>G: Require four fresh roles at 1.38 s ``` The one-millisecond delay follows station reassociation confirmation. It permits the AP to receive the response ACK and commit replacement before the joint query. Inactivity expiration remains disabled, so it cannot explain removal of the agreements. @@ -70,7 +72,7 @@ The one-millisecond delay follows station reassociation confirmation. It permits The first state assertion requires four old roles. The next frame assertion checks same-AP reassociation. After completion, the fixture requires equal peer rate sets and advertised HT capabilities. It then requires zero old roles before the new traffic batches start. -The later batches must trigger fresh ADDBA Requests in both directions. Concurrent observations require new traffic at UDP on both destination stations. At `1.4 s`, the fixture requires four fresh agreement roles. This final condition excludes a repair that clears state but prevents later setup. +The later batches must trigger fresh ADDBA Requests in both directions. Concurrent observations require new traffic at UDP on both destination stations. The batches continue until `1.4 s` to retain useful transfer context. At `1.38 s`, the fixture requires four fresh agreement roles. This final condition excludes a repair that clears state but prevents later setup. The test agent, `BlockAckReassociationAgent`, supplies only the reassociation request, state queries, and observation times. Production management performs the association transitions. Production HCF and its agreement handlers perform setup and cleanup. The agent never calls the peer reset operation. @@ -93,7 +95,7 @@ inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \ The file contains one simulation run with seed 0. Its required result is `PROTOCOLTEST wifi_block_ack_same_ap_reassociation: PASS`. -This test uses the Basic profile and successful same-AP reassociation. It does not cover another AP, refused reassociation, association loss, or replacement during an active DATA exchange. The state queries complement the frame trace. They do not establish full standards conformance. +This test uses the Basic profile and successful same-AP reassociation. It does not cover another AP, refused reassociation, or association loss. The state queries complement the frame trace. They do not establish full standards conformance. ## Profile limits diff --git a/doc/project/evidence/model/wifi/tests/blockack-transfer-completion-comparison.md b/doc/project/evidence/model/wifi/tests/blockack-transfer-completion-comparison.md new file mode 100644 index 00000000000..fca2a4dc976 --- /dev/null +++ b/doc/project/evidence/model/wifi/tests/blockack-transfer-completion-comparison.md @@ -0,0 +1,59 @@ +# Basic transfer completion: before and after the change + +> **Kind:** measurement · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [guide](blockack-transfer-completion.md) + +Both completion cases fail before S4 and pass on its final production source. The queue-exhaustion case misses BAR. The one-position-window case observes DATA where BAR is required. + +## Exact source and fixture inputs + +The parent is `02f24a3a4d519511cf24731e25d926068b11d24d`, with source tree `2a26acc1bd1d8d5ccac42c509cbfced219554175`. The final S4 source tree is `20aade533739a1877a87391909da7dd2a91c79c7`. + +The recorded controls use `WifiBlockAckTransferCompletion.test` with SHA-256 `e46b8804822747bc1558a802d0d7054aadf04b445370992bd5b5714b9864b0fa`. The shared topology and lifecycle inputs remain equal. No fixture supplies agreement state or injected feedback. + +The current fixture also rejects the program's FAIL line. This output guard changes no runtime scenario or protocol assertion. It prevents one successful run from hiding the other run's failure. + +## Verification of the output guard + +The corrected fixture has SHA-256 `7d526bcf8c213ebe7fdb4e4e5182393d546ef60e79feb4e044c97c3372d08156`. Both normal runs pass through the integrated debug protocol runner. Separate diagnostic INI files delay traffic beyond the protocol deadline in either run or both runs. Each simulation batch returns status 0, even when a protocol verdict is FAIL. + +The checks compare the same captured output against the previous fixture and the corrected fixture: + +| Run 0 | Run 1 | Previous aggregate | Corrected aggregate | +| --- | --- | --- | --- | +| PASS | PASS | PASS | PASS | +| FAIL | PASS | PASS | FAIL | +| PASS | FAIL | PASS | FAIL | +| FAIL | FAIL | FAIL | FAIL | + +These diagnostic failures are intentional controls, not failures of the normal completion scenarios. `opp_test check` returns 0 for PASS and 2 for FAIL, with zero harness errors. The guard rejects both mixed-result orders while it preserves the valid two-PASS result. + +The debug build log is `/tmp/pr1344-r1-debug.log`. The normal protocol log is `/tmp/pr1344-r1-protocol.log`. The verdict matrix is `/tmp/pr1344-r1-verdict-matrix.json`. Detailed checker logs use `/tmp/pr1344-r1--.log`. These local artifacts can be absent in another checkout. + +## Observed results + +| Run | Before S4 | After S4 | +| --- | --- | --- | +| 0: two packets, window 64 | FAIL: BAR deadline at step 3 expires. | PASS through matched feedback, DELBA, and ACK. | +| 1: four packets, window one | FAIL: the next DATA-or-BAR event fails the BAR predicate. | PASS through matched feedback, DELBA, and ACK. | + +Both parent failures occur after real association, successful ADDBA, and the first Block Ack DATA. They reach the intended protocol boundary. They do not arise from a build or initialization error. + +After S4, the policy selects BAR below threshold five. HCF permits BAR-only contention. The frame sequence receives matching feedback. Completion DELBA follows when no DATA remains. Zero inactivity timeout excludes inactivity teardown as an explanation. + +## Commands and evidence + +Build debug artifacts with `make MODE=debug -j12`. From each checkout root, run: + +```sh +inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiBlockAckTransferCompletion\.test$' +``` + +The parent command uses `/tmp/pr1344-controls-20261022`. Its combined selection also includes the Retry fixture and two existing lifecycle controls. Its log is `/tmp/pr1344-control-base-protocol.log`. Preserved outputs are under `/tmp/pr1344-parent-protocol-artifacts/WifiBlockAckTransferCompletion/`. + +The integrated run uses `/home/user/omnetpp_ws/inet-compressed-blockack-s4`. Its log is `/tmp/pr1344-correction-final-protocol-regression.log`. The completion fixture passes both runs. That wider selection also exposes two pre-existing lifecycle fixture assumptions; its aggregate failure is not a completion-test failure. + +General configuration runs 0 and 1 use seed 0 and a 1.1-second limit. The program stops when it reaches its verdict. The parent runner returns 1. The isolated final completion selection returns 0. + +## Limits + +These controls establish the two stated exchange sequences. The independent Retry correction does not create BAR-only access or completion DELBA. The exact reconstructed parent comparison receives its own verification record. No general performance, NAV, admission, or additional-seed claim follows from these results. diff --git a/doc/project/evidence/model/wifi/tests/blockack-transfer-completion.md b/doc/project/evidence/model/wifi/tests/blockack-transfer-completion.md new file mode 100644 index 00000000000..55450a7b0cf --- /dev/null +++ b/doc/project/evidence/model/wifi/tests/blockack-transfer-completion.md @@ -0,0 +1,53 @@ +# Basic transfer completion without further DATA + +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [test](../../../../../../tests/protocol/wifi/common/WifiBlockAckTransferCompletion.test), [comparison](blockack-transfer-completion-comparison.md) + +A short Basic transfer must request its final feedback and end with DELBA. The test observes this sequence through production MAC signals. It covers queue exhaustion and a transmit window smaller than the BAR threshold. + +## Scenario and requirement + +The network uses real infrastructure association and production ADDBA. A finite UDP burst starts at sta1 after association. The AP receives its uplink DATA. The test selects seed 0, threshold five, no A-MSDU aggregation, and zero inactivity timeout. + +IEEE Std 802.11-2024, 10.25.4 requires DELBA when the originator has no DATA and the final Block Ack exchange completes. The low-count BAR trigger is INET policy. Basic remains a compatibility profile rather than a current-standard Block Ack format claim. + +| Run | Application input | Accepted window | Trigger | +| --- | --- | --- | --- | +| 0 | Two UDP packets at 0.1 ms intervals. | 64 positions. | Initial Normal Ack DATA precedes one Block Ack DATA; the queue then empties. | +| 1 | Four UDP packets at 0.1 ms intervals. | One position. | One Block Ack DATA fills the window before another sequence can proceed. | + +## Assertions and production path + +The test observes ADDBA Request and successful Response. It captures the requested TID. It then observes the first DATA with Block Ack policy and captures its sequence. + +The next selected DATA-or-BAR transmission must be BAR for that TID and AP. Its starting sequence must equal the captured sequence. This assertion detects further DATA before required feedback. A later matching Block Ack must reach sta1. Completion then requires originator-role DELBA for the same TID and its ACK. + +The file requires a PASS line and rejects any FAIL line from this protocol program. A successful run cannot hide the other run's failure. Protocol verdicts end each simulation normally, so the process exit status alone cannot establish success. + +```mermaid +sequenceDiagram + participant O as Originator + participant R as Recipient + Note over O,R: Outstanding DATA below threshold five + Note over O: Queue empty or transmit window full + alt Before P4 + Note over O,R: No final feedback, transfer cannot complete + else After P4 + O->>R: BAR for the selected peer and TID + R-->>O: Matched Block Ack + Note over O: After all DATA resolves and none remains + O->>R: Completion DELBA + R-->>O: ACK + end +``` + +The fixture supplies no agreement, injected response, or production state edit. HCF, EDCA, the frame sequence, radio medium, and recipient execute the exchange. Inactivity cannot supply the required DELBA because its timeout is zero. + +## Cause and correction + +The parent policy requires the threshold before BAR. Its channel access also depends on DATA eligibility. The correction groups outstanding DATA by peer and TID, selects BAR before extraction, and permits BAR-only contention. Feedback resolves the group. HCF starts completion DELBA at the accepted boundary. + +The [reviewer document](../../../../../../tests/protocol/wifi/blockack-transfer-completion.md) associates this fixture with plan P4. The [comparison](blockack-transfer-completion-comparison.md) records actual outcomes. + +## Limits + +The test checks an observed exchange sequence. Existing module and unit fixtures check other peers, expiry, negative feedback, cyclic arithmetic, and timing boundaries. This fixture does not establish those additional properties, general Basic admission, full NAV correctness, or throughput. diff --git a/doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst b/doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst index 24be3196175..af9b1193673 100644 --- a/doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst +++ b/doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst @@ -3,4 +3,12 @@ IEEE 802.11 Basic Block Ack extension points HCF previews the next eligible DATA before BAR selection. Preview must preserve queue ownership and the next sequence number. Custom QoS data services must implement ``IOriginatorQosMacDataService``. This contract extends ``IOriginatorMacDataService`` with ``peekEligibleFrame()``, ``getNextSequenceNumber()``, and ``hasAMsduAggregation()``. +Basic single protection reserves only an already prepared successor. HCF ends the TXOP before further DATA extraction. A queued unit can change size or require agreement setup before transmission. Custom frame sequences must preserve this boundary when they use the same forecast. + +Custom QoS ACK policies must add ``IOriginatorBlockAckAgreementHandler *agreements`` to ``isBlockAckReqNeeded()`` and ``computeBlockAckReqParameters()``. The argument supplies current agreement windows. Policies must keep peer and TID identities separate. The supplied policy can request BAR without a DATA candidate. + +Callers must supply the agreement handler explicitly through both the interface and concrete policy types. An omitted argument fails compilation. Pass ``nullptr`` explicitly when no agreement handler is available; the supplied policy then requests no BAR. Custom overrides must also require the argument. + +Calls to ``QosAckHandler::processTransmittedBlockAckReq()`` must supply the originator agreement explicitly. The agreement restricts processing to the negotiated window. An explicit ``nullptr`` retains bitmap-only coverage without that restriction. Production HCF always supplies the active agreement. + Custom ``InProgressFrames`` classes must add ``bool extract = true`` to their ``getPendingFrameFor()`` override. When ``extract`` is false, the method must return a borrowed candidate without extraction or sequence assignment. The default value preserves ordinary extraction. Custom candidate sources must also implement ``peekFrameToTransmit()`` without extraction. diff --git a/doc/whatsnew/incompatible/ieee80211-basic-block-ack-transfer-completion.txt b/doc/whatsnew/incompatible/ieee80211-basic-block-ack-transfer-completion.txt new file mode 100644 index 00000000000..fa8e84be6c7 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-basic-block-ack-transfer-completion.txt @@ -0,0 +1,9 @@ +IEEE 802.11 Basic Block Ack transfer completion + +Callers must supply agreement arguments explicitly for BAR selection and transmitted-BAR processing. An omitted argument now fails compilation instead of silently selecting behavior without agreement state. + +Basic BAR selection now separates each peer and TID. Short bursts and small negotiated windows can cause BAR below the configured threshold. BAR can obtain channel access without queued DATA. After final completion or final DATA expiration, the originator sends DELBA even with zero inactivity timeout. Later bursts can require another ADDBA exchange. These changes affect exchange order, airtime, simulation results, and fingerprints for Basic Block Ack configurations. + +Basic single protection now reserves only an already prepared successor. Further DATA extraction starts a new TXOP and can require more contention. Final DATA in a zero-limit TXOP reserves only its immediate response. These corrections can change Duration fields, airtime, and fingerprints. + +Custom QoS ACK policies must accept the agreement-handler argument in isBlockAckReqNeeded() and computeBlockAckReqParameters(). Custom in-progress queues must preserve the extract argument in getPendingFrameFor(). QoS data services must implement IOriginatorQosMacDataService for candidate preview without extraction. diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index fdc8c44dd1e..e136518a229 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -170,7 +170,6 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); if (agreement) { - agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h index b7b9e4d6bc2..4b038c0c914 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h @@ -30,11 +30,10 @@ class INET_API Edca : public SimpleModule virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - virtual AccessCategory mapTidToAc(Tid tid); - public: virtual ~Edca(); + virtual AccessCategory mapTidToAc(Tid tid); virtual AccessCategory classifyFrame(const Ptr& header); virtual Edcaf *getEdcaf(AccessCategory ac) const { return edcafs[ac]; } EdcaTransmitLifetimeHandler *getLifetimeHandler() const { return lifetimeHandler; } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 94a583a111a..641e21e8ecd 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -10,6 +10,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" @@ -23,9 +24,9 @@ class INET_API IOriginatorQoSAckPolicy virtual bool isAckNeeded(const Ptr& header) const = 0; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const = 0; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) 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 *agreements) 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/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 1bbafa246de..1c8dbbdf508 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -204,6 +204,7 @@ void Hcf::expireDataFrames(bool preserveSequence) frames->clearDroppedFrames(); } scheduleDataLifetimeTimer(); + advanceOriginatorWindows(); } void Hcf::forEachChild(cVisitor *v) @@ -228,6 +229,8 @@ void Hcf::handleMessage(cMessage *msg) } else if (msg == dataLifetimeTimer) { expireDataFrames(); + if (!frameSequenceHandler->isSequenceRunning()) + completeTransfers(); updateAgreementProcedures(); updateContention(); } @@ -433,7 +436,10 @@ bool Hcf::hasQueuedData(MacAddress peer, Tid tid) const return data && data->getType() == ST_DATA_WITH_QOS && data->getReceiverAddress() == peer && data->getTid() == tid && !edca->getLifetimeHandler()->isExpired(packet); }); - for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + // Check the TID's category first. Other categories retain support for custom classification. + int first = edca->mapTidToAc(tid); + for (int offset = 0; offset < AC_NUMCATEGORIES; offset++) { + int ac = (first + offset) % AC_NUMCATEGORIES; auto queue = edca->getEdcaf(static_cast(ac))->getPendingQueue(); if (queue->isEmpty()) continue; @@ -513,6 +519,47 @@ void Hcf::updateContention() } } +void Hcf::advanceOriginatorWindows() +{ + if (!originatorBlockAckAgreementHandler) + return; + auto service = check_and_cast(originatorDataService); + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + auto agreement = entry.second; + if (!agreement->getIsAddbaResponseReceived()) + continue; + auto next = service->getNextSequenceNumber(entry.first.first, entry.first.second); + std::vector unresolvedSequences; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto frames = edca->getEdcaf(static_cast(ac))->getInProgressFrames(); + for (int i = 0; i < frames->getLength(); i++) { + auto data = dynamicPtrCast(frames->getFrames(i)->peekAtFront()); + if (data && data->getType() == ST_DATA_WITH_QOS && + data->getReceiverAddress() == entry.first.first && data->getTid() == entry.first.second) + unresolvedSequences.push_back(data->getSequenceNumber()); + } + } + // IEEE Std 802.11-2024, 10.25.6.7.1: resolved positions permit transmit-window progress. + agreement->advanceWindow(next, unresolvedSequences); + } +} + +void Hcf::completeTransfers() +{ + if (!originatorBlockAckAgreementHandler) + return; + std::vector> completed; + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + auto agreement = entry.second; + if (agreement->getIsAddbaResponseReceived() && agreement->getFirstDataTransmitted() && + !hasNumberedData(entry.first.first, entry.first.second) && !hasQueuedData(entry.first.first, entry.first.second)) + completed.push_back(entry.first); + } + // IEEE Std 802.11-2024, 10.25.4; INET uses the accepted queue predicate at an exchange boundary. + for (const auto& id : completed) + beginTeardown(id.first, id.second, true, 37); +} + void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex) { for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { @@ -893,6 +940,11 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) for (auto edcaf : internallyCollidedEdcafs) { AccessCategory ac = edcaf->getAccessCategory(); auto dataRecoveryProcedure = edcaf->getRecoveryProcedure(); + if (originatorAckPolicy->isBlockAckReqNeeded(edcaf->getInProgressFrames(), edcaf->getTxopProcedure(), originatorBlockAckAgreementHandler)) { + dataRecoveryProcedure->controlFrameTransmissionFailed(); + edcaf->requestChannel(this); + continue; + } Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); if (internallyCollidedFrame == nullptr) continue; @@ -954,6 +1006,8 @@ void Hcf::frameSequenceFinished() if (edcaf) { // The context still owns references. dropFrame defers deletion until context disposal. expireDataFrames(false); + advanceOriginatorWindows(); + completeTransfers(); updateAgreementProcedures(); bool startContention = !handlingAgreementAction && hasFrameToTransmit(); edcaf->releaseChannel(this); @@ -1285,8 +1339,12 @@ void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& controlHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) - edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq); + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(blockAckReq->getReceiverAddress(), blockAckReq->getTidInfo()) : nullptr; + if (agreement) + edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq, agreement); + } else if (auto rtsFrame = dynamicPtrCast(controlHeader)) rtsProcedure->processTransmittedRts(rtsFrame); else @@ -1326,6 +1384,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else if (auto blockAckReq = dynamicPtrCast(failedHeader)) { edcaf->getAckHandler()->processFailedBlockAckReq(blockAckReq); + edcaf->getRecoveryProcedure()->controlFrameTransmissionFailed(); return; } else @@ -1420,6 +1479,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); + advanceOriginatorWindows(); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { actionTerminated(lastTransmittedPacket, true); @@ -1427,8 +1487,12 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { + if (!originatorBlockAckAgreementHandler || + !originatorBlockAckAgreementHandler->getAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo())) + return; EV_INFO << "BasicBlockAck has arrived" << std::endl; edcaf->getRecoveryProcedure()->blockAckFrameReceived(); auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); @@ -1438,7 +1502,9 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetTag()->getMode() : rateSelection->computeMode(packet, header, txop); setFrameMode(packet, header, mode); + if (auto bar = dynamicPtrCast(header)) { + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), bar->getTidInfo()) : nullptr; + if (!agreement || !agreement->getIsAddbaResponseReceived()) + return false; + auto response = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + response; + // IEEE Std 802.11-2024, 10.23.2.8 and 10.23.2.9: a later BAR must fit both bounds. + if (context->getNumSteps() > 1 && + ((txop->getLimit() > 0 && ifs + cost > txop->getRemaining()) || + (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()))) + return false; + } if (scopedSingle) { auto sifs = modeSet->getSifsTime(); auto cost = mode->getDuration(packet->getDataLength()); @@ -1709,7 +1790,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) packet->insertAtFront(updated); } else if (txop->getProtectionMechanism() == TxopProcedure::ProtectionMechanism::SINGLE_PROTECTION) { - auto pendingPacket = channelOwner->getInProgressFrames()->getPendingFrameFor(packet); + auto pendingPacket = dynamicPtrCast(header) ? nullptr : + channelOwner->getInProgressFrames()->getPendingFrameFor(packet, false); + if (dynamicPtrCast(header) && txop->getLimit() == 0) { + // The zero-limit frame sequence ends after this exchange; it has no successor reservation. + pendingPacket = nullptr; + } + else if (pendingPacket && !channelOwner->getInProgressFrames()->containsFrame(pendingPacket)) { + // HcfFs ends this TXOP before extraction can change the successor's size or setup state. + pendingPacket = nullptr; + } const auto& pendingHeader = pendingPacket == nullptr ? nullptr : pendingPacket->peekAtFront(); auto duration = singleProtectionMechanism->computeDurationField(packet, header, pendingPacket, pendingHeader, txop, recipientAckPolicy); auto header = packet->removeAtFront(); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 1361083c32d..25fa8a0bb90 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -159,6 +159,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: bool hasNumberedData(MacAddress peer, Tid tid) const; bool hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const; void updateAgreementProcedures(); + void advanceOriginatorWindows(); + void completeTransfers(); void updateContention(); void scheduleAddbaResponseTimer(); void actionTerminated(Packet *packet, bool acknowledged); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index b12ce4d54f4..7a49e0ece7c 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -34,8 +34,12 @@ HcfFs::HcfFs() : int HcfFs::selectHcfSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) { - auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - return frameToTransmit->peekAtFront()->getReceiverAddress().isMulticast() ? 0 : 1; + auto qos = context->getQoSContext(); + auto frameToTransmit = qos->blockAckAgreementHandler ? context->getInProgressFrames()->peekFrameToTransmit() : + context->getInProgressFrames()->getFrameToTransmit(); + if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) + return 1; + return frameToTransmit && frameToTransmit->peekAtFront()->getReceiverAddress().isMulticast() ? 0 : 1; } int HcfFs::selectDataOrManagementSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) @@ -57,15 +61,21 @@ bool HcfFs::isSelfCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *con bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *context) { - bool hasFrameToTransmit = context->getInProgressFrames()->hasInProgressFrames(); + auto frames = context->getInProgressFrames(); + auto qos = context->getQoSContext(); + bool bar = qos->ackPolicy->isBlockAckReqNeeded(frames, qos->txopProcedure, qos->blockAckAgreementHandler); + bool hasFrameToTransmit = qos->blockAckAgreementHandler ? + bar || frames->peekFrameToTransmit() != nullptr : frames->hasInProgressFrames(); if (hasFrameToTransmit) { - auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - const auto& nextHeader = nextFrameToTransmit->peekAtFront(); + auto nextFrameToTransmit = qos->blockAckAgreementHandler ? frames->peekFrameToTransmit() : frames->getFrameToTransmit(); if (frameSequence->getCount() == 0) return true; - auto qos = context->getQoSContext(); auto txop = qos->txopProcedure; - if (nextHeader->getReceiverAddress().isMulticast() || txop->getRemaining() <= 0) + // Basic single protection reserves only prepared MPDUs. Unextracted DATA starts a new TXOP. + if (qos->blockAckAgreementHandler && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && + !bar && !frames->containsFrame(nextFrameToTransmit)) + return false; + if ((!bar && nextFrameToTransmit->peekAtFront()->getReceiverAddress().isMulticast()) || txop->getRemaining() <= 0) return false; if (txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION) { auto deadline = txop->getStart() + txop->getLimit(); @@ -77,8 +87,7 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte if (projectedStart >= deadline) return false; // BAR precedes DATA in TxOpFs and does not use RTS. - if (!qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), txop) && - context->getRtsPolicy()->isRtsNeeded(nextFrameToTransmit, nextHeader)) + if (!bar && context->getRtsPolicy()->isRtsNeeded(nextFrameToTransmit, nextFrameToTransmit->peekAtFront())) { // Integer legacy RTS airtime requires its start on the deadline's microsecond grid. if ((projectedStart - deadline).raw() % SimTime(1, SIMTIME_US).raw() != 0) diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 3fda1fcd52c..f9a3de4a6e5 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -412,7 +412,8 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con { switch (step) { case 0: { - auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure); + auto qos = context->getQoSContext(); + auto blockAckReqParams = qos->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler); auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); @@ -445,7 +446,12 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) step++; auto receivedPacket = receiveStep->getReceivedFrame(); const auto& receivedHeader = receivedPacket->peekAtFront(); - return context->isForUs(receivedHeader) && receivedHeader->getType() == ST_BLOCKACK; + auto response = dynamicPtrCast(receivedHeader); + auto requestStep = check_and_cast(context->getStep(firstStep)); + auto request = requestStep->getFrameToTransmit()->peekAtFront(); + return context->isForUs(receivedHeader) && response && + response->getTransmitterAddress() == request->getReceiverAddress() && + response->getTidInfo() == request->getTidInfo(); } 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 02420634437..e0daefabaa7 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc @@ -44,10 +44,11 @@ int TxOpFs::selectMgmtOrDataQap(AlternativesFs *frameSequence, FrameSequenceCont int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) { + auto qos = context->getQoSContext(); + if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) + return 2; auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); - if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure)) - return 2; if (dynamicPtrCast(macHeader)) return 3; else { diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index b99e0a3339b..dbdfdf50d0b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -31,67 +31,55 @@ bool OriginatorQosAckPolicy::isAckNeeded(const Ptr& h return !header->getReceiverAddress().isMulticast(); } -std::map> OriginatorQosAckPolicy::getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const +bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *frames, TxopProcedure *txop, IOriginatorBlockAckAgreementHandler *agreements) const { - auto outstandingFrames = inProgressFrames->getOutstandingFrames(); - std::map> outstandingFramesPerReceiver; - for (auto frame : outstandingFrames) - outstandingFramesPerReceiver[frame->peekAtFront()->getReceiverAddress()].push_back(frame); - return outstandingFramesPerReceiver; + return std::get<2>(computeBlockAckReqParameters(frames, txop, agreements)) != -1; } -SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const std::vector& outstandingFrames) const +std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *frames, TxopProcedure *txop, IOriginatorBlockAckAgreementHandler *agreements) const { - ASSERT(outstandingFrames.size() > 0); - auto startingSequenceNumber = outstandingFrames[0]->peekAtFront()->getSequenceNumber(); - for (size_t i = 1; i < outstandingFrames.size(); i++) { - auto seqNum = outstandingFrames[i]->peekAtFront()->getSequenceNumber(); - if (seqNum < startingSequenceNumber) - startingSequenceNumber = seqNum; + using Id = std::pair; + std::map> groups; + for (auto frame : frames->getOutstandingFrames()) { + auto header = frame->peekAtFront(); + auto agreement = agreements ? agreements->getAgreement(header->getReceiverAddress(), header->getTid()) : nullptr; + if (agreement && agreement->getIsAddbaResponseReceived() && agreement->containsSequence(header->getSequenceNumber())) + groups[{header->getReceiverAddress(), header->getTid()}].push_back(frame); } - return startingSequenceNumber; -} - -bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) 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; -} - -// FIXME -bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const -{ - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) - return true; - } - return false; -} - -// FIXME -std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) 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; + if (groups.empty()) + return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(0), Tid(-1)); + auto candidate = frames->peekFrameToTransmit(); + auto data = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + auto result = std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(0), Tid(-1)); + simtime_t oldestArrival = SIMTIME_MAX; + for (const auto& entry : groups) { + const auto& id = entry.first; + auto agreement = agreements->getAgreement(id.first, id.second); + // INET BAR policy defers a short group only for DATA within that group's window. + if ((int)entry.second.size() < blockAckReqThreshold && data && + data->getReceiverAddress() == id.first && data->getTid() == id.second && + agreement->containsSequence(frames->getCandidateSequenceNumber(candidate))) + continue; + auto start = agreement->getStartingSequenceNumber(); + int oldestOffset = -1; + simtime_t arrival = SIMTIME_MAX; + for (auto frame : entry.second) { + auto sequence = frame->peekAtFront()->getSequenceNumber(); + int offset = OriginatorBlockAckAgreement::computeSequenceDistance(start, sequence); + if (oldestOffset == -1 || offset < oldestOffset) { + oldestOffset = offset; + arrival = frame->getArrivalTime(); } - MacAddress receiverAddress = largestOutstandingFrames->first; - SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); - Tid tid = largestOutstandingFrames->second.at(0)->peekAtFront()->getTid(); - return std::make_tuple(receiverAddress, startingSequenceNumber, tid); + else if (offset == oldestOffset) + arrival = std::min(arrival, frame->getArrivalTime()); + } + // Map order supplies the peer/TID tie break. + if (std::get<2>(result) == -1 || arrival < oldestArrival) { + oldestArrival = arrival; + result = std::make_tuple(id.first, start + oldestOffset, id.second); } } - return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(), -1); + return result; } AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const @@ -117,10 +105,9 @@ bool OriginatorQosAckPolicy::isBlockAckPolicyEligibleFrame(Packet *packet, const bool OriginatorQosAckPolicy::checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const { - bool bufferFull = agreement->getBufferSize() == agreement->getNumSentBaPolicyFrames(); bool aMsduOk = agreement->isAMsduPermitted() || !header->getAMsduPresent(); // TODO bool baPolicy = agreement->getIsDelayedBlockAckPolicySupported() || !frame->getAckPolicy(); - return !bufferFull && aMsduOk && (header->getSequenceNumber() >= agreement->getStartingSequenceNumber()); // TODO && baPolicy + return aMsduOk && agreement->containsSequence(header->getSequenceNumber()); } // diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index bcd9f1b2ae7..cefd7008567 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -31,16 +31,13 @@ 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 SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; - virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const; 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 *agreements) const override; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 2fa02655743..c79877d4a72 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -147,6 +147,13 @@ std::set>> QosAckHand else { throw cRuntimeError("Multi-TID BlockReq is unimplemented"); } + auto basic = dynamicPtrCast(blockAck); + auto compressed = dynamicPtrCast(blockAck); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + for (auto& entry : ackStatuses) { + if (entry.first.first == receiverAddr && entry.first.second.first == tid && entry.second == Status::WAITING_FOR_BLOCK_ACK) + entry.second = Status::BLOCK_ACK_ARRIVED_UNACKED; + } return ackedFrames; } @@ -195,23 +202,28 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const PtrgetReceiverAddress(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()))] = Status::WAITING_FOR_NORMAL_ACK; } -void QosAckHandler::processTransmittedBlockAckReq(const Ptr& blockAckReq) +void QosAckHandler::processTransmittedBlockAckReq(const Ptr& blockAckReq, const OriginatorBlockAckAgreement *agreement) { for (auto& ackStatus : ackStatuses) { + if (ackStatus.first.first != blockAckReq->getReceiverAddress()) + continue; auto tid = ackStatus.first.second.first; auto seqCtrlField = ackStatus.first.second.second; + auto sequence = SequenceNumberCyclic(seqCtrlField.getSequenceNumber()); + if (agreement && !agreement->containsSequence(sequence)) + continue; auto& status = ackStatus.second; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { if (basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum) + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && OriginatorBlockAckAgreement::isInBitmap(sequence, startingSeqNum)) status = Status::WAITING_FOR_BLOCK_ACK; } } else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { if (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)? + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && OriginatorBlockAckAgreement::isInBitmap(sequence, startingSeqNum) && seqCtrlField.getFragmentNumber() == 0) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 7c1e0bce61c..96f1026d85f 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -13,6 +13,7 @@ #include "inet/linklayer/ieee80211/mac/common/SequenceControlField.h" #include "inet/linklayer/ieee80211/mac/contract/IAckHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -60,7 +61,7 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; virtual void processTransmittedDataOrMgmtFrame(const Ptr& header); - virtual void processTransmittedBlockAckReq(const Ptr& blockAckReq); + virtual void processTransmittedBlockAckReq(const Ptr& blockAckReq, const OriginatorBlockAckAgreement *agreement); virtual void processFailedFrame(const Ptr& dataOrMgmtHeader); virtual void dropFrame(const Ptr& dataOrMgmtHeader); virtual void dropFrames(std::set>> seqAndFragNums); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc index e0b177a94b2..e52f6b72652 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc @@ -110,6 +110,20 @@ void QosRecoveryProcedure::ctsFrameReceived() void QosRecoveryProcedure::blockAckFrameReceived() { resetStationSrc(); + resetContentionWindow(); +} + +void QosRecoveryProcedure::controlFrameTransmissionFailed() +{ + // IEEE Std 802.11-2024, 10.23.2.2: control failures update EDCA backoff without DATA retry feedback. + if (stationShortRetryCounter < shortRetryLimit) { + stationShortRetryCounter++; + incrementContentionWindow(); + } + else { + resetStationSrc(); + resetContentionWindow(); + } } // diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h index 69435073efc..e1804fcc323 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h @@ -66,6 +66,7 @@ class INET_API QosRecoveryProcedure : public SimpleModule, public IRecoveryProce virtual void ctsFrameReceived(); virtual void ackFrameReceived(Packet *packet, const Ptr& ackedHeader); virtual void blockAckFrameReceived(); + virtual void controlFrameTransmissionFailed(); virtual void rtsFrameTransmissionFailed(const Ptr& protectedHeader); virtual void dataFrameTransmissionFailed(Packet *packet, const Ptr& failedHeader); diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 81ec8985ddc..416e9709ad3 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -179,7 +179,8 @@ std::vector InProgressFrames::getOutstandingFrames() { std::vector outstandingFrames; for (auto frame : inProgressFrames) { - if (ackHandler->isOutstandingFrame(frame->peekAtFront())) + if ((!lifetimeHandler || !lifetimeHandler->isExpired(frame)) && + ackHandler->isOutstandingFrame(frame->peekAtFront())) outstandingFrames.push_back(frame); } return outstandingFrames; diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index e4a86e712bd..fed121062af 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -7,7 +7,7 @@ # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, dd36-1076/tplx;b703-3ef2/~tNl;e07d-2980/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 5fbb-9fd9/tplx;c0a4-97f6/~tNl;2373-df01/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 4fd2-bb56/tplx;ecaf-89f7/~tNl;9398-0fae/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 2f2d-73b1/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 2c80-6844/tplx;4e9e-cf86/~tNl;ea3d-e802/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -658,7 +658,7 @@ /examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, d929-5913/tplx;f602-4424/~tNl;f2fb-4d98/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 0473-f9d2/tplx;e92c-7352/~tNl;317d-aa4f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, e07a-4c5f/tplx;9f6c-7889/~tNl;29ea-169c/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, f8a4-9474/tplx;1cba-cccc/~tNl;1914-cd4d/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 6edb-0326/tplx;2a8a-50ee/~tNl;4301-882d/~tND;08b5-d005/tyf, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 9416b34980b..9baf2771fb6 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -199,9 +199,9 @@ /showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;7b7c-2bfb/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;d680-b8ad/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, b828-6bd6/tplx;026f-2c6d/~tNl;0f38-6042/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 021a-d29a/tplx;1597-ec6a/~tNl;ddec-9e3d/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 5b9e-801f/tplx;8731-2504/~tNl;2839-8753/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 9161-625d/tplx;6a02-09aa/~tNl;cbc0-3b0a/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 36f4-5c22/tplx;66b0-94cd/~tNl;45f3-af4f/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 351b-d4ba/tplx;5c1f-69a8/~tNl;6182-6da3/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;fc60-7eb7/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;6f3a-074a/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -267,7 +267,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 8df2-40a3/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, c99f-354d/tplx;8e10-a3f0/~tNl;ac4b-3ba1/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, f509-65f1/tplx;6b3a-0d65/~tNl;bbe5-0771/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 2597-1e2e/tplx;d269-c6ae/~tNl;2f5c-4cca/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 6150-7a8b/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -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, f58d-d2fd/tplx;38f7-f642/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, ab91-87f3/tplx;f30c-6aa4/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 diff --git a/tests/module/Ieee80211BlockAckCompletionCost_1.test b/tests/module/Ieee80211BlockAckCompletionCost_1.test new file mode 100644 index 00000000000..358aee3883e --- /dev/null +++ b/tests/module/Ieee80211BlockAckCompletionCost_1.test @@ -0,0 +1,210 @@ +%description: +Measure production Basic setup and transfer completion with equal UDP payload totals. +One run sends twelve packets continuously. The other run sends three bursts of four packets. +Both runs use seed zero, 6 Mbit/s, zero inactivity timeout, and a fixed 100 ms interval. +The measurements describe these traffic shapes. They do not estimate agreement-reuse performance. + +%extraargs: -r 0..1 + +%file: CompletionCost.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/common/TimeTag_m.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class CompletionCostHcf : public Hcf { + simtime_t deferredAt = -1; + simtime_t requestQueuedAt = -1; + simtime_t requestSentAt = -1; + simtime_t delbaQueuedAt = -1; + public: + simtime_t deferral; + simtime_t requestContention; + simtime_t responseAfterRequest; + simtime_t delbaCompletion; + protected: + void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& header, AccessCategory ac) override { + Hcf::originatorProcessTransmittedDataFrame(packet, header, ac); + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + if (procedures.count({header->getReceiverAddress(), header->getTid()}) && deferredAt < 0) + deferredAt = simTime(); + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + requestQueuedAt = simTime(); + if (dynamicPtrCast(header)) + delbaQueuedAt = simTime(); + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto request = dynamicPtrCast(packet->peekAtFront()); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && request) { + requestContention += simTime() + ifs - requestQueuedAt; + requestSentAt = simTime() + ifs; + } + return accepted; + } + void processLowerFrame(Packet *packet, const Ptr& header) override { + bool response = dynamicPtrCast(header) != nullptr; + Hcf::processLowerFrame(packet, header); + if (response && deferredAt >= 0) { + deferral += simTime() - deferredAt; + responseAfterRequest += simTime() - requestSentAt; + deferredAt = -1; + } + } + void originatorProcessReceivedFrame(Packet *packet, Packet *transmitted) override { + bool delba = dynamicPtrCast(transmitted->peekAtFront()) != nullptr; + Hcf::originatorProcessReceivedFrame(packet, transmitted); + if (delba) + delbaCompletion += simTime() - delbaQueuedAt; + } +}; +Define_Module(CompletionCostHcf); +class CompletionCostTest : public cSimpleModule, public cListener { + cModule *sender = nullptr; + cModule *sink = nullptr; + int sent = 0; + int received = 0; + int requests = 0; + int responses = 0; + int delbas = 0; + int drops = 0; + int64_t payloadBytes = 0; + simtime_t managementAirtime; + simtime_t deliveryDelay; + protected: + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + auto packet = check_and_cast(value); + if (source == sender) { + sent++; + return; + } + if (source == sink) { + received++; + payloadBytes += packet->getByteLength(); + auto creation = packet->peekAtFront()->getTag()->getCreationTime(); + deliveryDelay += simTime() - creation; + return; + } + if (signal == packetDroppedSignal) { + drops++; + return; + } + auto header = packet->peekAtFront(); + if (dynamicPtrCast(header)) + requests++; + if (dynamicPtrCast(header)) + responses++; + if (dynamicPtrCast(header)) + delbas++; + if (dynamicPtrCast(header)) + managementAirtime += packet->getTag()->getMode()->getDuration(packet->getDataLength()); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + sender = getModuleByPath("^.host1.app[0]"); + sink = getModuleByPath("^.host2.app[0]"); + sender->subscribe(packetSentSignal, this); + sink->subscribe(packetReceivedSignal, this); + for (const char *path : {"^.host1.wlan[0].mac.hcf", "^.host2.wlan[0].mac.hcf"}) { + getModuleByPath(path)->subscribe(packetSentToPeerSignal, this); + getModuleByPath(path)->subscribe(packetDroppedSignal, this); + } + scheduleAt(SimTime(99, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + auto hcf = check_and_cast(getModuleByPath("^.host1.wlan[0].mac.hcf")); + std::cout << "Completion cost: gaps=" << par("gaps").boolValue() << " sent=" << sent + << " received=" << received << " bytes=" << payloadBytes << " requests=" << requests + << " responses=" << responses << " delbas=" << delbas << " drops=" << drops + << " managementAirtime=" << managementAirtime << " deferral=" << hcf->deferral + << " requestContention=" << hcf->requestContention << " responseAfterRequest=" << hcf->responseAfterRequest + << " delbaCompletion=" << hcf->delbaCompletion << " deliveryDelaySum=" << deliveryDelay + << " goodputBps=" << payloadBytes * 8 / 0.1 << "\n"; + ASSERT(sent == 12 && received == 12 && payloadBytes == 8400 && drops == 0); + ASSERT(requests == (par("gaps").boolValue() ? 3 : 1)); + ASSERT(responses == requests && delbas == requests); + std::cout << "Block Ack completion cost assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(CompletionCostTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.AdhocHost; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module CompletionCostHcf extends Hcf { parameters: @class(::inet::ieee80211::CompletionCostHcf); } +simple CompletionCostTest { parameters: bool gaps; @class(::inet::ieee80211::CompletionCostTest); } +network CompletionCostNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host1: AdhocHost; + host2: AdhocHost; + test: CompletionCostTest; +} + +%inifile: omnetpp.ini +[General] +network = CompletionCostNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.test.gaps = ${gaps=false,true} +*.host1.numApps = 1 +*.host1.app[0].typename = "UdpBasicBurst" +*.host1.app[0].destAddresses = "host2" +*.host1.app[0].destPort = 1000 +*.host1.app[0].messageLength = 700B +*.host1.app[0].chooseDestAddrMode = "once" +*.host1.app[0].startTime = 10ms +*.host1.app[0].stopTime = ${gaps} ? 52ms : 11.15ms +*.host1.app[0].sendInterval = 100us +*.host1.app[0].burstDuration = ${gaps} ? 350us : 0s +*.host1.app[0].sleepDuration = ${gaps} ? 20ms : 0s +*.host2.numApps = 1 +*.host2.app[0].typename = "UdpSink" +*.host2.app[0].localPort = 1000 +*.host*.ipv4.arp.typename = "GlobalArp" +*.host*.wlan[*].opMode = "a" +*.host*.wlan[*].bitrate = 6Mbps +*.host*.wlan[*].classifier.typename = "QosClassifier" +*.host*.wlan[*].mac.qosStation = true +*.host*.wlan[*].mac.hcf.typename = "CompletionCostHcf" +*.host*.wlan[*].mac.hcf.isBlockAckSupported = true +*.host*.wlan[*].mac.hcf.blockAckVariant = "basic" +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +*.host*.wlan[*].radio.receiver.energyDetection = -105dBm +*.host*.wlan[*].radio.receiver.sensitivity = -105dBm +*.host*.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.host1.mobility.initialX = 0m +*.host2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack completion cost assertions passed. diff --git a/tests/module/Ieee80211BlockAckDeferral_1.test b/tests/module/Ieee80211BlockAckDeferral_1.test index 89da8e14084..58f6614bc96 100644 --- a/tests/module/Ieee80211BlockAckDeferral_1.test +++ b/tests/module/Ieee80211BlockAckDeferral_1.test @@ -29,6 +29,8 @@ class DeferralHcf : public Hcf { int dataVoice = 0; int dataVideo = 0; int dataAfterRequest = 0; + int dataWithAgreement = 0; + int dataAfterDelba = 0; int aggregatesAccepted = 0; int requestStart = -1; int firstData = -1; @@ -118,6 +120,12 @@ class DeferralHcf : public Hcf { if (data->getTid() == 6) dataVoice++; if (data->getTid() == 5) dataVideo++; if (requestStart >= 0 && data->getTid() == 6) { + if (hasAgreement(6)) + dataWithAgreement++; + if (delbas > 0 && !hasAgreement(6)) { + ASSERT(data->getAckPolicy() == NORMAL_ACK); + dataAfterDelba++; + } if (firstData < 0) firstData = data->getSequenceNumber().get(); dataAfterRequest++; } @@ -203,7 +211,7 @@ class DeferralTest : public cSimpleModule, public cListener { << ", aggregates=" << sender->aggregatesAccepted << ", request start=" << sender->requestStart << ", first DATA=" << sender->firstData << ".\n"; ASSERT(deferredBeforeExtraction); - ASSERT(sender->requests == 1 && sender->hasAgreement(6)); + ASSERT(sender->requests == 1 && !sender->hasAgreement(6) && sender->delbas == 1); ASSERT(sender->requestStart == 1 && sender->firstData == sender->requestStart); ASSERT(sender->dataAfterRequest == 1); ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 2 : 0)); @@ -249,10 +257,16 @@ class DeferralTest : public cSimpleModule, public cListener { wait(SimTime(560, SIMTIME_MS) - simTime()); ASSERT(sender->rtsFailures >= 1); ASSERT(sender->firstRtsSequence == sender->requestStart); - ASSERT(sender->dataAfterRequest == 0 && sender->firstData == -1); + ASSERT(sender->dataWithAgreement == 0); + if (scenario == 8) { + ASSERT(sender->dataAfterRequest == 1 && sender->dataAfterDelba == 1); + ASSERT(sender->firstData == (sender->requestStart + 1) % 4096); + } + else + ASSERT(sender->dataAfterRequest == 0 && sender->firstData == -1); ASSERT(!sender->hasAgreement(6)); ASSERT(sender->delbas == 1); - ASSERT(lifetimeDrops == (scenario == 9 ? 1 : 0)); + ASSERT(lifetimeDrops == (scenario == 9 ? 2 : 0)); } else if (scenario >= 5) { auto edca = check_and_cast(sender->getSubmodule("edca")); @@ -271,7 +285,8 @@ class DeferralTest : public cSimpleModule, public cListener { accept(sender, 6); } wait(SimTime(513, SIMTIME_MS) - simTime()); - ASSERT(lifetimeDrops == 1 && lifetime->findFrame(deferredId) == nullptr); + // Preview leaves both unsent frames under their original lifetime deadlines. + ASSERT(lifetimeDrops == 2 && lifetime->findFrame(deferredId) == nullptr); if (scenario == 6) ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 0); else { @@ -284,7 +299,7 @@ class DeferralTest : public cSimpleModule, public cListener { << ", response deadline=" << sender->observedDeadline << ", original DATA deadline=0.512.\n"; } else if (scenario == 2) { - ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->dataAfterRequest == 2); ASSERT(sender->firstData == sender->requestStart); } else { @@ -315,9 +330,12 @@ class DeferralTest : public cSimpleModule, public cListener { ASSERT(!sender->hasAgreement(6)); } else { - ASSERT(sender->delbas == 0); - ASSERT(sender->hasAgreement(6)); - ASSERT(sender->dataAfterRequest == 1); + // The accepted response releases the final DATA before completion causes DELBA. + ASSERT(simTime() < sender->observedDeadline); + ASSERT(sender->delbas == 1); + ASSERT(!sender->hasAgreement(6)); + std::cout << "Completed deferred DATA: " << sender->dataAfterRequest << ".\n"; + ASSERT(sender->dataAfterRequest == 2); ASSERT(sender->firstData == sender->requestStart); } } diff --git a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test index 33429c70e6a..47377708224 100644 --- a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test +++ b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test @@ -2,7 +2,7 @@ Check the sequence boundary after ordinary originator teardown of fragmented DATA. IEEE Std 802.11-2024, 11.5.3.4 requires the next MPDU to use a later sequence when the originator discards the last transmitted MPDU. -The fixture supplies accepted Basic agreements and injects immediate ACKs. +The fixture supplies accepted Basic agreements and injects immediate ACKs and Basic Block Ack responses. Production fragmentation, inactivity expiry, retirement, and transmission remain active. Runs cover sequence 0 and wraparound at 4095, with expiry during transmission or after the first fragment receives its Normal Ack. @@ -10,6 +10,7 @@ Later intact DATA and an unrelated peer must retain their transmissions. Additional runs transmit an earlier DATA unit before a gap in sequence allocation. The fixture extracts and disposes of the skipped DATA through the production allocator. Distances 2047, 2048, 2049, and 4096 cover the half-range boundary and a full wrap. +A deferred successor keeps the transfer open while the fixture advances the allocator. %extraargs: -r 0..19 @@ -45,6 +46,9 @@ class FragmentRetirementHcf : public Hcf { bool retired = false; bool successorSent = false; bool otherSent = false; + bool otherRetired = false; + Ptr pendingBar; + Packet *deferredSuccessor = nullptr; int delbas = 0; void skipData(int count) { @@ -75,7 +79,7 @@ class FragmentRetirementHcf : public Hcf { scheduleAfter(SimTime(acknowledgedFirst ? 650 : 974, SIMTIME_US), new cMessage("queue-data")); } - void queueData(const char *name, MacAddress address, int length) { + Packet *queueData(const char *name, MacAddress address, int length) { auto header = makeShared(); header->setType(ST_DATA_WITH_QOS); header->addChunkLength(QOSCONTROL_PART_LENGTH); @@ -87,6 +91,7 @@ class FragmentRetirementHcf : public Hcf { packet->insertAtBack(makeShared(B(length))); packet->insertAtBack(makeShared()); processUpperFrame(packet, header); + return packet; } public: @@ -106,9 +111,13 @@ class FragmentRetirementHcf : public Hcf { if (distance == 0) startRetirement(); else { + check_and_cast(originatorDataService)->setEligibility( + [this](const Packet *packet) { return !packet->isName("deferred-successor") && isFrameEligible(packet, false); }, + [this](Packet *packet) { return isAMsduPermitted(packet); }); // Force Normal Ack for the earlier DATA, independent of the fragment ACK policy. handler->getAgreement(peer, 5)->setStartingSequenceNumber(SequenceNumberCyclic(start) + 1); queueData("earlier-data", peer, 20); + deferredSuccessor = queueData("deferred-successor", peer, 20); } scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verify")); } @@ -119,6 +128,13 @@ class FragmentRetirementHcf : public Hcf { Hcf::receiveSignal(source, signal, object, details); return; } + auto agreement = check_and_cast(object); + if (agreement->getReceiverAddr() == other) { + ASSERT(otherSent); + ASSERT(!hasQueuedData(other, 5) && !hasNumberedData(other, 5)); + otherRetired = true; + return; + } ASSERT(!retired); retired = true; ASSERT(firstCompleted == acknowledgedFirst); @@ -164,6 +180,11 @@ class FragmentRetirementHcf : public Hcf { void transmissionComplete(Packet *packet, const Ptr& header) override { Enter_Method("transmissionComplete"); auto data = dynamicPtrCast(header); + auto bar = dynamicPtrCast(header); + if (bar) { + ASSERT(bar->getReceiverAddress() == other); + pendingBar = bar; + } bool earlier = packet->isName("earlier-data"); bool first = !earlier && data && data->getReceiverAddress() == peer && data->getSequenceNumber() == SequenceNumberCyclic(start); bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); @@ -174,6 +195,8 @@ class FragmentRetirementHcf : public Hcf { Hcf::transmissionComplete(packet, header); if (ack) scheduleAfter(SimTime(20, SIMTIME_US), new cMessage(earlier ? "ack-earlier" : first ? "ack-first" : "ack")); + if (bar) + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage("block-ack")); } void handleMessage(cMessage *message) override { @@ -183,6 +206,24 @@ class FragmentRetirementHcf : public Hcf { queueData("intact-successor", peer, 20); queueData("other-peer", other, 20); } + else if (message->isName("block-ack")) { + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + header->setTransmitterAddress(other); + header->setTidInfo(5); + header->setStartingSequenceNumber(pendingBar->getStartingSequenceNumber()); + BitVector bits; + bits.appendBit(true); + for (int i = 1; i < 16; i++) + bits.appendBit(false); + header->setBlockAckBitmap(0, bits); + auto packet = new Packet("block-ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(24000000))); + pendingBar = nullptr; + processLowerFrame(packet, header); + } else if (message->isName("ack") || message->isName("ack-first") || message->isName("ack-earlier")) { bool first = message->isName("ack-first"); bool earlier = message->isName("ack-earlier"); @@ -196,6 +237,12 @@ class FragmentRetirementHcf : public Hcf { if (first) firstAcknowledged = true; if (earlier) { + auto queue = edca->getEdcaf(AC_VI)->getPendingQueue(); + queue->removePacket(deferredSuccessor); + take(deferredSuccessor); + edca->getLifetimeHandler()->forgetFrame(deferredSuccessor->getId()); + delete deferredSuccessor; + deferredSuccessor = nullptr; skipData(distance - 1); start = (start + distance) % 4096; startRetirement(); @@ -203,9 +250,11 @@ class FragmentRetirementHcf : public Hcf { } else if (message->isName("verify")) { delete message; + std::cout << "Fragment retirement state: retired=" << retired << " firstCompleted=" << firstCompleted + << " successorSent=" << successorSent << " otherSent=" << otherSent << " delbas=" << delbas << "\n"; ASSERT(retired && firstCompleted && successorSent && otherSent); - ASSERT(delbas == 1); - ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) != nullptr); + ASSERT(delbas == 2 && otherRetired); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) == nullptr); unsubscribe(blockAckAgreementDeletedSignal, this); std::cout << "Fragment retirement passed: start=" << start << ", acknowledgedFirst=" << acknowledgedFirst << ", distance=" << distance << std::endl; diff --git a/tests/module/Ieee80211BlockAckScheduling_1.test b/tests/module/Ieee80211BlockAckScheduling_1.test new file mode 100644 index 00000000000..3fe472f6f68 --- /dev/null +++ b/tests/module/Ieee80211BlockAckScheduling_1.test @@ -0,0 +1,455 @@ +%description: +Exercise Basic BAR selection and final DELBA through HCF, frame sequences, EDCA, and Tx. +The fixture supplies accepted agreements and injects responses at the HCF receive boundary. +Response events include SIFS and the selected response airtime. +Cases cover short bursts, windows smaller than the threshold, two peers, BAR timeout, +negative bitmap bits, Normal Ack window progress, and late BAR admission. + +%extraargs: -r 0..29 + +%file: Scheduling.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#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/linklayer/ieee80211/mac/Rx.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class SchedulingRecipientAgreements : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; +class SchedulingRecipient : public Hcf { + public: + std::vector delivered; + bool checkedBitmap = false; + void prepare() { + Enter_Method("prepare"); + delete recipientBlockAckAgreementHandler; + auto agreements = new SchedulingRecipientAgreements(); + recipientBlockAckAgreementHandler = agreements; + agreements->accept(MacAddress("02:00:00:00:00:01")); + subscribe(blockAckAgreementDeletedSignal, this); + } + void receive(Packet *packet) { + Enter_Method("receive"); + auto header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + if (data->getSequenceNumber() == SequenceNumberCyclic(0) && data->getFragmentNumber() != 0) { + delete packet; + return; + } + } + auto mode = packet->getTag()->getMode(); + packet->addTagIfAbsent()->setMode(mode); + processLowerFrame(packet, header); + } + bool retired() const { return recipientBlockAckAgreementHandler->getAgreements().empty(); } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == blockAckAgreementDeletedSignal) + ASSERT((delivered == std::vector{1, 2})); + else + Hcf::receiveSignal(source, signal, value, details); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered.push_back(frame->peekAtFront()->getSequenceNumber().get()); + take(frame); + delete frame; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr& header, + Packet *, const Ptr&) override { + if (auto response = dynamicPtrCast(header)) { + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(response->getBlockAckBitmap(0).getBit(0)); + ASSERT(!response->getBlockAckBitmap(0).getBit(1)); + if (response->getBlockAckBitmap(1).getBit(0) && response->getBlockAckBitmap(2).getBit(0)) { + checkedBitmap = true; + ASSERT(delivered.empty()); + } + } + delete packet; + } +}; +Define_Module(SchedulingRecipient); +class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, int size) { + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTid(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setBufferSize(size); + request->setBlockAckPolicy(true); + createAgreement(request); + auto agreement = getAgreement(peer, 5); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + finishProcedure(peer, 5); + } +}; +class SchedulingHcf : public Hcf { + const MacAddress peer{"02:00:00:00:00:02"}; + const MacAddress other{"02:00:00:00:00:03"}; + int dataCount = 0; + int bars = 0; + int delbas = 0; + int refusedBars = 0; + int retries = 0; + int dataInTxop = 0; + bool responsePending = false; + Ptr pendingBar; + const physicallayer::IIeee80211Mode *pendingResponseMode = nullptr; + bool firstBarWasStandalone = false; + int expired = 0; + int collisions = 0; + SchedulingRecipient *recipient = nullptr; + simtime_t responseEnd; + simtime_t txopDeadline; + int scenario() const { return par("scenario"); } + int burst() const { + switch (scenario()) { + case 0: return 1; + case 1: return 4; + case 2: return 6; + case 3: return 9; + case 4: return 4; + case 5: return 1; + case 6: return 1; + case 7: return 2; + case 9: return 3; + case 10: return 1; + case 11: return 8; + case 12: return 1; + case 13: return 1; + case 14: return 1; + default: return 4; + } + } + int window() const { return scenario() == 2 || scenario() == 7 ? 1 : scenario() == 3 ? 4 : 64; } + public: + void begin() { + Enter_Method("begin"); + delete originatorBlockAckAgreementHandler; + auto agreements = new SchedulingAgreements(); + originatorBlockAckAgreementHandler = agreements; + agreements->accept(peer, window()); + if (scenario() == 9) { + recipient = check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf")); + recipient->prepare(); + subscribe(packetDroppedSignal, this); + } + if (scenario() == 4) + agreements->accept(other, 64); + rx->setContentionBlocked(true); + for (int i = 0; i < burst(); i++) + queueData(peer, scenario() == 9 ? (i == 0 ? 200 : 10) : scenario() == 7 && i == 0 ? 1100 : 100); + if (scenario() == 12 || scenario() == 14) { + auto frames = edca->getEdcaf(AC_VI)->getInProgressFrames(); + auto frame = frames->getFrameToTransmit(); + auto header = frame->removeAtFront(); + header->setAckPolicy(BLOCK_ACK); + frame->insertAtFront(header); + edca->getEdcaf(AC_VI)->getAckHandler()->processTransmittedDataOrMgmtFrame(header); + originatorBlockAckAgreementHandler->getAgreement(peer, 5)->recordTransmittedData( + frame->getTag()->getAllocationIndex()); + dataCount = 1; + queueData(other, 1100, 6); + } + if (scenario() == 4) { + for (int i = 0; i < 8; i++) + queueData(other, 100); + } + rx->setContentionBlocked(false); + scheduleAfter(SimTime(40, SIMTIME_MS), new cMessage("verify")); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == packetDroppedSignal) { + auto drop = dynamic_cast(details); + if (drop && drop->getReason() == LIFETIME_EXPIRED) + expired++; + if (source == this) + return; + } + Hcf::receiveSignal(source, signal, value, details); + } + void queueData(MacAddress receiver, int length, Tid tid = 5) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(receiver); + auto packet = new Packet("data", header); + packet->insertAtBack(makeShared(B(length))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + void frameSequenceStarted() override { + dataInTxop = 0; + Hcf::frameSequenceStarted(); + } + void channelGranted(IChannelAccess *access) override { + auto collided = edca->getInternallyCollidedEdcafs(); + if (scenario() == 12 || scenario() == 14) + collisions += collided.size(); + Hcf::channelGranted(access); + if (scenario() == 14 && !collided.empty()) { + auto edcaf = edca->getEdcaf(AC_VI); + ASSERT(edcaf->getCw() == 1); + auto frame = edcaf->getInProgressFrames()->getFrames(0); + bool absent = false; + try { + edcaf->getRecoveryProcedure()->getRetryCount(frame, frame->peekAtFront()); + } + catch (const cRuntimeError& error) { + absent = std::string(error.what()).find("retry counter entry doesn't exist") != std::string::npos; + } + ASSERT(absent); + } + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + auto bar = dynamicPtrCast(header); + if (bar) { + auto frames = edca->getChannelOwner()->getInProgressFrames(); + auto candidate = frames->peekFrameToTransmit(); + if (candidate && !frames->containsFrame(candidate)) { + auto sequence = candidate->peekAtFront()->getSequenceNumber(); + auto size = frames->getLength(); + ASSERT(frames->peekFrameToTransmit() == candidate); + ASSERT(frames->getLength() == size); + ASSERT(candidate->peekAtFront()->getSequenceNumber() == sequence); + } + } + bool accepted = Hcf::transmitFrame(packet, ifs); + if (bar && !accepted) { + refusedBars++; + return false; + } + if (!accepted) + return false; + header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + auto txop = edca->getChannelOwner()->getTxopProcedure(); + if (!par("multiple").boolValue() && txop->getLimit() == 0 && data->getAckPolicy() == BLOCK_ACK) + ASSERT(data->getDurationField() == SIMTIME_ZERO); + if (!par("multiple").boolValue() && scenario() == 3) { + ASSERT(dataInTxop == 0); + ASSERT(data->getDurationField() == SIMTIME_ZERO); + } + if (scenario() == 4 && data->getReceiverAddress() == other) + ASSERT(bars >= 1); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + if ((scenario() == 12 || scenario() == 14) && data->getTid() == 6) + ASSERT(!data->getRetry()); + else + ASSERT(agreement != nullptr && agreement->containsSequence(data->getSequenceNumber())); + if (scenario() != 7 && data->getSequenceNumber().get() >= window() && data->getReceiverAddress() == peer) + ASSERT(bars >= 1); + if (data->getRetry()) + retries++; + dataCount++; + dataInTxop++; + } + if (bar) { + bars++; + if (bars == 1) { + firstBarWasStandalone = dataInTxop == 0; + ASSERT(bar->getReceiverAddress() == peer); + ASSERT(bar->getTidInfo() == 5); + if (scenario() != 9) + ASSERT(dataCount == (scenario() == 2 ? 1 : scenario() == 3 ? 4 : scenario() == 7 || scenario() == 12 || scenario() == 14 ? 2 : scenario() == 11 ? 5 : burst())); + } + auto mode = packet->getTag()->getMode(); + auto responseMode = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_BASIC_BLOCKACK); + auto txop = edca->getChannelOwner()->getTxopProcedure(); + auto end = simTime() + ifs + cost; + if (dataInTxop > 1 && txop->getLimit() > 0) + ASSERT(end <= txop->getStart() + txop->getLimit()); + std::cout << "BAR timing: scenario=" << scenario() << " multiple=" << par("multiple").boolValue() + << " start=" << simTime() << " ifs=" << ifs << " bytes=" << packet->getByteLength() + << " rate=" << mode->getDataMode()->getNetBitrate() << " responseRate=" << responseMode->getDataMode()->getNetBitrate() + << " response=" << responseMode->getDuration(LENGTH_BASIC_BLOCKACK) << " end=" << end + << " deadline=" << txop->getStart() + txop->getLimit() << " data=" << dataInTxop << "\n"; + } + if (auto delba = dynamicPtrCast(header)) { + ASSERT(delba->getInitiator()); + ASSERT(!hasQueuedData(delba->getReceiverAddress(), delba->getTid())); + ASSERT(!hasNumberedData(delba->getReceiverAddress(), delba->getTid())); + delbas++; + } + return true; + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto bar = dynamicPtrCast(header); + auto data = dynamicPtrCast(header); + if (recipient && (data || bar || dynamicPtrCast(header))) + recipient->receive(packet->dup()); + bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); + if (bar || ack) { + if (!(bar && scenario() == 5 && bars == 1)) { + pendingBar = bar; + responsePending = true; + pendingResponseMode = bar ? + rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT) : + rateSelection->computeResponseAckFrameMode(packet, dynamicPtrCast(header), ResponseQueryRole::PREDICT); + auto duration = pendingResponseMode->getDuration(bar ? LENGTH_BASIC_BLOCKACK : LENGTH_ACK); + responseEnd = simTime() + modeSet->getSifsTime() + duration; + txopDeadline = edca->getChannelOwner()->getTxopProcedure()->getStart() + edca->getChannelOwner()->getTxopProcedure()->getLimit(); + scheduleAt(responseEnd, new cMessage("response")); + } + } + Hcf::transmissionComplete(packet, header); + } + bool isReceptionInProgress() override { return responsePending || Hcf::isReceptionInProgress(); } + void handleMessage(cMessage *message) override { + if (message->isName("response")) { + delete message; + responsePending = false; + Ptr header; + if (pendingBar) { + auto response = makeShared(); + response->setTransmitterAddress(pendingBar->getReceiverAddress()); + response->setTidInfo(pendingBar->getTidInfo()); + response->setStartingSequenceNumber(pendingBar->getStartingSequenceNumber()); + for (int i = 0; i < 64; i++) { + BitVector bits; + for (int j = 0; j < 16; j++) + bits.appendBit(scenario() == 9 ? (i != 0 || j == 0) : !(scenario() == 6 && bars == 1)); + response->setBlockAckBitmap(i, bits); + } + header = response; + } + else + header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("response", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(pendingResponseMode); + pendingBar = nullptr; + processLowerFrame(packet, header); + } + else if (message->isName("verify")) { + delete message; + std::cout << "Scheduling totals: data=" << dataCount << " bars=" << bars << " delbas=" << delbas << " refused=" << refusedBars << "\n"; + ASSERT(bars > 0); + ASSERT(delbas == (scenario() == 4 ? 2 : 1)); + if (scenario() != 9) + ASSERT(dataCount == burst() + (scenario() == 4 ? 8 : scenario() == 5 || scenario() == 6 || scenario() == 12 || scenario() == 14 ? 1 : 0)); + else { + ASSERT(recipient->checkedBitmap && recipient->retired()); + ASSERT((recipient->delivered == std::vector{1, 2})); + ASSERT(dataCount > 3); + ASSERT(expired > 0); + } + ASSERT(originatorBlockAckAgreementHandler->getAgreements().empty()); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().empty()); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(!frameSequenceHandler->isSequenceRunning()); + if (scenario() == 5 || scenario() == 6) + ASSERT(retries == 1 && bars == 2); + if (scenario() == 8 || scenario() == 10 || scenario() == 13) + ASSERT(firstBarWasStandalone); + if (scenario() == 12 || scenario() == 14) + ASSERT(collisions == 1 && retries == 0); + std::cout << "Block Ack scheduling assertions passed.\n"; + endSimulation(); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(SchedulingHcf); +class SchedulingTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(SchedulingTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module SchedulingHcf extends Hcf { parameters: int scenario; bool multiple; @class(::inet::ieee80211::SchedulingHcf); } +module SchedulingRecipient extends Hcf { parameters: @class(::inet::ieee80211::SchedulingRecipient); } +simple SchedulingTest { parameters: @class(::inet::ieee80211::SchedulingTest); } +network SchedulingNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + recipient: WirelessHost; + test: SchedulingTest; +} + +%inifile: omnetpp.ini +[General] +network = SchedulingNetwork +ned-path = .;../../../../src +sim-time-limit = 50ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +*.host.wlan[*].address = "02:00:00:00:00:01" +*.recipient.wlan[*].address = "02:00:00:00:00:02" +**.wlan[*].mac.qosStation = true +*.recipient.wlan[*].mac.hcf.typename = "SchedulingRecipient" +*.host.wlan[*].mac.hcf.typename = "SchedulingHcf" +**.wlan[*].mac.hcf.scenario = ${scenario=0..14} +**.wlan[*].mac.hcf.multiple = ${multiple=false,true} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.wlan[*].mac.hcf.originatorAckPolicy.blockAckTimeout = 1ms +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 2346B +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = ${scenario} == 9 ? 128B : 2346B +**.wlan[*].mac.hcf.edca.edcaf[*].msduLifetime = ${scenario} == 9 ? 10ms : 512ms +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[2].cwMax = ${scenario} == 14 ? 3 : ${scenario} == 12 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[2].recoveryProcedure.shortRetryLimit = ${scenario} == 14 ? 1 : 7 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = ${multiple} ? "multiple" : "single" +**.wlan[*].mac.hcf.edca.edcaf[2].txopProcedure.txopLimit = ${scenario} == 8 ? 900us : ${scenario} == 10 ? 0s : ${scenario} == 11 ? 1160us : ${scenario} == 13 ? 50us : 10ms +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 2000000m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.recipient.mobility.initialX = 1000000m +*.host.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack scheduling assertions passed. diff --git a/tests/module/Ieee80211HcfHeader_1.test b/tests/module/Ieee80211HcfHeader_1.test index 81703858adf..595c2b1b6b7 100644 --- a/tests/module/Ieee80211HcfHeader_1.test +++ b/tests/module/Ieee80211HcfHeader_1.test @@ -20,7 +20,7 @@ class HeaderAckPolicy : public OriginatorQosAckPolicy { selections++; return BLOCK_ACK; } - bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *) const override { return false; } + bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return false; } }; Define_Module(HeaderAckPolicy); class HeaderAgreementHandler : public OriginatorBlockAckAgreementHandler { diff --git a/tests/module/Ieee80211HcfMultipleProtection_2.test b/tests/module/Ieee80211HcfMultipleProtection_2.test index 748c19d83ef..1d278478756 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_2.test +++ b/tests/module/Ieee80211HcfMultipleProtection_2.test @@ -116,6 +116,7 @@ Define_Module(RateProtectionMac); class RateProtectionHcf : public Hcf { public: int acknowledgments = 0; + int managementAcknowledgments = 0; int blockAcks = 0; int failures = 0; void seedOriginator() { @@ -168,7 +169,12 @@ class RateProtectionHcf : public Hcf { } void originatorProcessReceivedFrame(Packet *packet, Packet *request) override { auto type = packet->peekAtFront()->getType(); - if (type == ST_ACK) acknowledgments++; + if (type == ST_ACK) { + if (dynamicPtrCast(request->peekAtFront())) + acknowledgments++; + else + managementAcknowledgments++; + } if (type == ST_BLOCKACK) blockAcks++; Hcf::originatorProcessReceivedFrame(packet, request); } @@ -208,12 +214,13 @@ class RateProtectionTest : public cSimpleModule { else { sender->inject(66, scenario == 6); if (scenario == 0 || scenario == 1) - sender->inject(); // The existing BAR branch needs a next selected frame. + sender->inject(); } wait(SimTime(5, SIMTIME_MS)); if (scenario == 0 || scenario == 1) { ASSERT(sender->blockAcks >= 1); ASSERT(sender->acknowledgments == 0); + ASSERT(sender->managementAcknowledgments == 1); ASSERT(std::find(senderMac->types.begin(), senderMac->types.end(), ST_BLOCKACK_REQ) != senderMac->types.end()); ASSERT(receiverMac->types[0] == ST_CTS); auto firstRtsEndpoint = senderMac->ends[0] + senderMac->durations[0]; diff --git a/tests/protocol/wifi/README.md b/tests/protocol/wifi/README.md index 5d5798887a9..4cfb4aa3b6f 100644 --- a/tests/protocol/wifi/README.md +++ b/tests/protocol/wifi/README.md @@ -37,24 +37,25 @@ Each protocol test has two documents. The guide explains its scenario, assertion | `WifiBlockAckInactivity.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-inactivity.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-inactivity-comparison.md) | | `WifiBlockAckSameApReassociation.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-same-ap-reassociation-comparison.md) | | `WifiBlockAckDataRetry.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-data-retry.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md) | +| `WifiBlockAckTransferCompletion.test` | [Guide](../../../doc/project/evidence/model/wifi/tests/blockack-transfer-completion.md) | [Before and after](../../../doc/project/evidence/model/wifi/tests/blockack-transfer-completion-comparison.md) | The [shared procedure](../../../doc/project/evidence/model/wifi/pr1343-comparison-method.md) explains how to reproduce each comparison with identical fixture inputs. ## Source inventory -The suite contains **81 tests: 51 default PASS expectations and 30 declared FAIL expectations**. +The suite contains **82 tests: 52 default PASS expectations and 30 declared FAIL expectations**. These are source declarations, not a fresh full-suite result. | Directory | Tests | Expected PASS | Expected FAIL | | --- | ---: | ---: | ---: | -| `common/` | 25 | 16 | 9 | +| `common/` | 26 | 17 | 9 | | `legacy/` | 6 | 5 | 1 | | `11b/` | 6 | 4 | 2 | | `11a/` | 4 | 4 | 0 | | `11g/` | 7 | 6 | 1 | | `11n/` | 18 | 11 | 7 | | `11ac/` | 15 | 5 | 10 | -| **Total** | **81** | **51** | **30** | +| **Total** | **82** | **52** | **30** | The [audit report](../../../doc/project/evidence/model/wifi/results.md) owns the observed results, failure classifications, repair dependencies, and verification records. It includes diff --git a/tests/protocol/wifi/blockack-transfer-completion.md b/tests/protocol/wifi/blockack-transfer-completion.md new file mode 100644 index 00000000000..612d3df7751 --- /dev/null +++ b/tests/protocol/wifi/blockack-transfer-completion.md @@ -0,0 +1,32 @@ +# Basic transfer completion below the BAR threshold + +Plan P4, **Complete Basic BAR transfer resolution**, connects outstanding DATA to channel access and final DELBA. The test is [WifiBlockAckTransferCompletion.test](common/WifiBlockAckTransferCompletion.test). + +Before this commit, a short group can lack feedback because the BAR threshold is five. Channel access also requires another DATA candidate. Queue exhaustion or a full transmit window can therefore prevent transfer completion. + +After this commit, the ACK policy can select BAR below the threshold. HCF requests channel access for that BAR before DATA extraction. Matched feedback resolves the group. When no queued or numbered DATA remains, HCF queues DELBA. IEEE Std 802.11-2024, 10.25.4 supplies the completion requirement. + +The fixture uses real association, production ADDBA, and finite UDP bursts. Run 0 sends two packets with an accepted window of 64. Run 1 sends four packets with a one-position window. Both retain threshold five. The next selected DATA-or-BAR transmission after the first Block Ack DATA must be BAR. The test also requires matched feedback, originator-role DELBA, and its ACK. + +The output checks require PASS and reject any FAIL from this program. Both runs must succeed; one passing run cannot hide the other's failure. + +```mermaid +sequenceDiagram + participant O as Originator + participant R as Recipient + Note over O,R: Outstanding DATA below threshold five + Note over O: Queue empty or transmit window full + alt Before P4 + Note over O,R: No final feedback, transfer cannot complete + else After P4 + O->>R: BAR for the selected peer and TID + R-->>O: Matched Block Ack + Note over O: After all DATA resolves and none remains + O->>R: Completion DELBA + R-->>O: ACK + end +``` + +The [guide](../../../doc/project/evidence/model/wifi/tests/blockack-transfer-completion.md) defines setup and assertions. The [comparison](../../../doc/project/evidence/model/wifi/tests/blockack-transfer-completion-comparison.md) records executed results and limits. + +Basic remains an INET compatibility profile. These cases do not establish complete NAV conformance, general Basic DATA admission, or a performance result. diff --git a/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test b/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test index 4f9cb137cdd..1b68c527b62 100644 --- a/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test +++ b/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test @@ -34,10 +34,21 @@ Define_Module(LifecycleBarMac); class LifecycleBarStimulus : public cSimpleModule, public ITx::ICallback { protected: - void initialize() override { scheduleAt(SimTime(1100, SIMTIME_MS), new cMessage("bar")); } + void initialize() override { + if (par("control").boolValue()) + scheduleAt(SimTime(1002, SIMTIME_MS), new cMessage("hold-contention")); + scheduleAt(SimTime(1100, SIMTIME_MS), new cMessage("bar")); + } void handleMessage(cMessage *message) override { + bool hold = message->isName("hold-contention"); delete message; auto network = getParentModule(); + if (hold) { + // Retained queued DATA prevents completion DELBA before the control BAR. + auto mac = network->getSubmodule("sta1")->getSubmodule("wlan", 0)->getSubmodule("mac"); + check_and_cast(mac->getSubmodule("rx"))->setContentionBlocked(true); + return; + } bool control = par("control"); Tid tid = control ? 6 : 5; auto sender = lifecycleHcf(network, "sta1"); diff --git a/tests/protocol/wifi/common/WifiBlockAckSameApReassociation.test b/tests/protocol/wifi/common/WifiBlockAckSameApReassociation.test index 7c9d66ed285..6c25407193d 100644 --- a/tests/protocol/wifi/common/WifiBlockAckSameApReassociation.test +++ b/tests/protocol/wifi/common/WifiBlockAckSameApReassociation.test @@ -4,6 +4,7 @@ Same-AP reassociation retains identical peer rates and HT capabilities, with ina Both endpoints must clear both agreement roles before a new traffic batch starts. The new batch must establish fresh agreements and reach both UDP destinations. Read-only queries observe agreement state; the fixture never supplies agreements or calls reset. +The fixture requests reassociation before the old traffic completes. %file: SameApReassociation.cc #include "BlockAckLifecycleTestSupport.h" @@ -29,7 +30,10 @@ class BlockAckReassociationAgent : public Ieee80211AgentSta apHcf->hasOriginator(station, 6) + apHcf->hasRecipient(station, 6); } public: - ~BlockAckReassociationAgent() { cancelAndDelete(replace); cancelAndDelete(verify); } + ~BlockAckReassociationAgent() { + cancelAndDelete(replace); + cancelAndDelete(verify); + } protected: void processAssociateConfirm(Ieee80211Prim_AssociateConfirm *response) override { bool success = response->getResultCode() == PRC_SUCCESS; @@ -37,7 +41,7 @@ class BlockAckReassociationAgent : public Ieee80211AgentSta if (success && !scheduled) { scheduled = true; replace = new cMessage("same-ap-reassociation"); - scheduleAt(SimTime(1100, SIMTIME_MS), replace); + scheduleAt(SimTime(1008, SIMTIME_MS), replace); } } void processReassociateConfirm(Ieee80211Prim_ReassociateConfirm *response) override { @@ -90,7 +94,7 @@ class BlockAckReassociationAgent : public Ieee80211AgentSta emit(registerSignal("equalPeerParameters"), long(equal)); emit(registerSignal("rolesAfterReplacement"), long(agreementRoles())); verify = new cMessage("verify-fresh-setup"); - scheduleAt(SimTime(1400, SIMTIME_MS), verify); + scheduleAt(SimTime(1380, SIMTIME_MS), verify); } else Ieee80211AgentSta::handleMessage(message); @@ -147,6 +151,13 @@ simple BlockAckReassociationAgent extends Ieee80211AgentSta { %inifile: test.ini [General] +**.wlan[*].bitrate = 6Mbps +**.originatorMacDataService.msduAggregationPolicy.typename = "" +# Only the AP-sta1 link needs Block Ack for the reset assertions. +*.sta2.wlan[*].mac.hcf.isBlockAckSupported = false +*.ap.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 500B +# Slotted bidirectional contention can produce simultaneous transmissions. +*.radioMedium.sameTransmissionStartTimeCheck = "warning" *.tester.testName = "wifi_block_ack_same_ap_reassociation" *.sta1.wlan[*].agent.typename = "BlockAckReassociationAgent" *.sta1.numApps = 3 @@ -160,17 +171,17 @@ simple BlockAckReassociationAgent extends Ieee80211AgentSta { *.sta1.app[2].messageLength = 1000B *.sta1.app[2].sendInterval = 0.1ms *.sta1.app[2].startTime = 1.3s -*.sta1.app[2].stopTime = 1.303s +*.sta1.app[2].stopTime = 1.4s *.sta2.numApps = 3 *.sta2.app[1..2].typename = "UdpBasicApp" *.sta2.app[1..2].destAddresses = "sta1" *.sta2.app[1..2].destPort = 5001 -*.sta2.app[1..2].messageLength = 1000B +*.sta2.app[1..2].messageLength = 100B *.sta2.app[1..2].sendInterval = 0.1ms -*.sta2.app[1].startTime = 1.01s -*.sta2.app[1].stopTime = 1.013s +*.sta2.app[1].startTime = 1.000001s +*.sta2.app[1].stopTime = 1.003001s *.sta2.app[2].startTime = 1.31s -*.sta2.app[2].stopTime = 1.313s +*.sta2.app[2].stopTime = 1.4s sim-time-limit = 1.5s include ../../ini/_blockackLifecycle.ini diff --git a/tests/protocol/wifi/common/WifiBlockAckTransferCompletion.test b/tests/protocol/wifi/common/WifiBlockAckTransferCompletion.test new file mode 100644 index 00000000000..f3510b4d32c --- /dev/null +++ b/tests/protocol/wifi/common/WifiBlockAckTransferCompletion.test @@ -0,0 +1,76 @@ +%description: +Production Basic transfers below the BAR threshold must request final feedback and end with DELBA. +The two-packet case exhausts the queue after one Block Ack DATA frame. +The four-packet case uses a one-position window and requires BAR before more DATA. +Both cases use real association, production ADDBA, threshold five, and seed zero. +IEEE Std 802.11-2024, 10.25.4 supplies the final-transfer teardown requirement. +Basic remains an INET compatibility profile. + +%extraargs: -r 0..1 + +%file: BlockAckTransferCompletion.cc +#include "BlockAckLifecycleTestSupport.h" + +namespace inet::protocoltest { +Define_ProtocolTest(wifi_block_ack_transfer_completion) +{ + return ProtocolTest("wifi_block_ack_transfer_completion") + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("has(Ieee80211AddbaRequest)") + .capture("tid", "Ieee80211AddbaRequest.tid") + .describe("originator requests the production agreement").within(1.05)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("has(Ieee80211AddbaResponse)") + .assertPacket("Ieee80211AddbaResponse.statusCode == 0 && Ieee80211AddbaResponse.tid == {tid}") + .describe("recipient accepts the agreement").within(0.01)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + auto data = dynamicPtrCast(c.event.packet->peekAtFront()); + return data && data->getAckPolicy() == BLOCK_ACK; + }) + .capture("firstSequence", [](const PacketEvent& event) { + return cValue(event.packet->peekAtFront()->getSequenceNumber().get()); + }) + .describe("standalone DATA uses the accepted agreement").within(0.01)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + auto header = c.event.packet->peekAtFront(); + return dynamicPtrCast(header) != nullptr || header->getType() == ST_DATA_WITH_QOS; + }) + .assertEvent([](const MatchContext& c) { + auto bar = dynamicPtrCast(c.event.packet->peekAtFront()); + return bar && bar->getTidInfo() == c.get("tid").intValue() && + bar->getReceiverAddress() == MacAddress("02:00:00:00:00:01") && + bar->getStartingSequenceNumber().get() == c.get("firstSequence").intValue(); + }) + .describe("BAR proceeds below threshold five").within(0.01)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("has(Ieee80211BasicBlockAck)") + .assertPacket("Ieee80211BasicBlockAck.tidInfo == {tid}") + .describe("matching Block Ack resolves the group").within(0.01)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("has(Ieee80211Delba)") + .assertPacket("Ieee80211Delba.initiator == true && Ieee80211Delba.tid == {tid}") + .describe("originator ends the completed transfer").within(0.03)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 29") + .describe("recipient acknowledges completion DELBA").within(0.01)); +} +} + +%inifile: test.ini +[General] +cmdenv-stop-batch-on-error = false +*.tester.testName = "wifi_block_ack_transfer_completion" +*.sta1.app[0].sendInterval = 0.1ms +*.sta1.app[0].stopTime = ${smallWindow=false,true} ? 1.00035s : 1.00015s +**.recipientBlockAckAgreementPolicy.maximumAllowedBufferSize = ${smallWindow} ? 1 : 64 +**.originatorMacDataService.msduAggregationPolicy.typename = "" +sim-time-limit = 1.1s +include ../../ini/_blockackLifecycle.ini + +%contains: stdout +PROTOCOLTEST wifi_block_ack_transfer_completion: PASS + +%not-contains: stdout +PROTOCOLTEST wifi_block_ack_transfer_completion: FAIL diff --git a/tests/unit/Ieee80211BlockAckScheduling_1.test b/tests/unit/Ieee80211BlockAckScheduling_1.test new file mode 100644 index 00000000000..c0fd4049ab9 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckScheduling_1.test @@ -0,0 +1,153 @@ +%description: +Check peer/TID BAR arbitration, window boundaries, cyclic sequence order, and ACK-state isolation. +The production policy reads the production ACK handler through an in-progress queue. +These helper checks supplement the HCF module fixture. + +%includes: +#include +#include +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +using namespace inet; +using namespace inet::ieee80211; + +namespace Ieee80211BlockAckScheduling_1 { +template struct ImplicitBarQuery : std::false_type {}; +template struct ImplicitBarQuery().isBlockAckReqNeeded(nullptr, nullptr))>> : std::true_type {}; +template struct ImplicitBarParameters : std::false_type {}; +template struct ImplicitBarParameters().computeBlockAckReqParameters(nullptr, nullptr))>> : std::true_type {}; +template struct ImplicitBarWindow : std::false_type {}; +template struct ImplicitBarWindow().processTransmittedBlockAckReq(std::declval>()))>> : std::true_type {}; + +static_assert(!ImplicitBarQuery::value, "The interface requires an explicit agreement handler"); +static_assert(!ImplicitBarQuery::value, "The concrete policy requires an explicit agreement handler"); +static_assert(!ImplicitBarParameters::value, "The interface requires an explicit agreement handler"); +static_assert(!ImplicitBarParameters::value, "The concrete policy requires an explicit agreement handler"); +static_assert(!ImplicitBarWindow::value, "The ACK handler requires an explicit agreement window"); + +class SchedulingCw : public IRecoveryProcedure::ICwCalculator { + public: + int cw = 0; + void incrementCw() override { cw = std::min(7, 2 * cw + 1); } + void resetCw() override { cw = 0; } + int getCw() override { return cw; } +}; +class SchedulingRecovery : public QosRecoveryProcedure { + public: + void configure(ICwCalculator *calculator) { cwCalculator = calculator; shortRetryLimit = 1; } + int getStationSrc() const { return stationShortRetryCounter; } + bool hasDataRetry() const { return !shortRetryCounter.empty() || !longRetryCounter.empty(); } +}; +class SchedulingPolicy : public OriginatorQosAckPolicy { + public: + SchedulingPolicy() { blockAckReqThreshold = 5; } +}; +class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { + public: + OriginatorBlockAckAgreement *accept(MacAddress peer, Tid tid, int start, int size) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), size, false, false); + agreement->setIsAddbaResponseReceived(true); + blockAckAgreements[{peer, tid}] = agreement; + return agreement; + } +}; +class SchedulingFrames : public InProgressFrames { + public: + Packet *candidate = nullptr; + void configure(QosAckHandler *handler) { ackHandler = handler; } + Packet *peekFrameToTransmit() override { return candidate; } + Ptr add(MacAddress peer, Tid tid, int sequence, bool sent = true) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setAckPolicy(BLOCK_ACK); + auto packet = new Packet("data", header); + take(packet); + packet->setArrivalTime(simTime()); + inProgressFrames.push_back(packet); + ackHandler->frameGotInProgress(header); + if (sent) + check_and_cast(ackHandler)->processTransmittedDataOrMgmtFrame(header); + return header; + } +}; +} + +%activity: +SchedulingCw cw; +SchedulingRecovery recovery; +recovery.configure(&cw); +recovery.controlFrameTransmissionFailed(); +ASSERT(recovery.getStationSrc() == 1 && cw.getCw() == 1); +recovery.controlFrameTransmissionFailed(); +ASSERT(recovery.getStationSrc() == 0 && cw.getCw() == 0); +recovery.controlFrameTransmissionFailed(); +recovery.blockAckFrameReceived(); +ASSERT(recovery.getStationSrc() == 0 && cw.getCw() == 0 && !recovery.hasDataRetry()); +const MacAddress peer("02:00:00:00:00:02"); +const MacAddress other("02:00:00:00:00:03"); +SchedulingPolicy policy; +SchedulingAgreements agreements; +QosAckHandler ack; +SchedulingFrames frames; +frames.configure(&ack); +agreements.accept(peer, 2, 100, 8); +auto selectedAgreement = agreements.accept(peer, 5, 4094, 4); +auto otherAgreement = agreements.accept(other, 5, 4094, 4); +std::vector> unrelated; +for (int i = 100; i < 104; i++) + unrelated.push_back(frames.add(peer, 2, i)); +auto selected = frames.add(peer, 5, 4095); +auto wrap = frames.add(peer, 5, 0); +auto outside = frames.add(peer, 5, 2); +auto separatePeer = frames.add(other, 5, 4095); +// A candidate inside TID 2 defers only that group. +frames.add(peer, 2, 104, false); +frames.candidate = frames.getFrames(frames.getLength() - 1); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, nullptr)); +ASSERT(std::get<2>(policy.computeBlockAckReqParameters(&frames, nullptr, nullptr)) == -1); +auto choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 5); +ASSERT(std::get<1>(choice) == SequenceNumberCyclic(4095)); +ASSERT(selectedAgreement->containsSequence(SequenceNumberCyclic(1))); +ASSERT(!selectedAgreement->containsSequence(SequenceNumberCyclic(2))); +auto bar = makeShared(); +bar->setReceiverAddress(peer); +bar->setTidInfo(5); +bar->setStartingSequenceNumber(SequenceNumberCyclic(4095)); +ack.processTransmittedBlockAckReq(bar, selectedAgreement); +ASSERT(ack.getQoSDataAckStatus(selected) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); +ASSERT(ack.getQoSDataAckStatus(wrap) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); +for (auto header : unrelated) + ASSERT(ack.getQoSDataAckStatus(header) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ASSERT(ack.getQoSDataAckStatus(outside) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ASSERT(ack.getQoSDataAckStatus(separatePeer) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ack.processFailedBlockAckReq(bar); +ASSERT(ack.isEligibleToTransmit(selected)); +ASSERT(ack.isEligibleToTransmit(wrap)); +ASSERT(ack.getQoSDataAckStatus(separatePeer) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +frames.candidate = nullptr; +choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 2); +ASSERT(std::get<1>(choice) == SequenceNumberCyclic(100)); +ASSERT(otherAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(4094)); +auto duration = SimTime(1, SIMTIME_US); +EdcaTransmitLifetimeHandler lifetime(duration, duration, duration, duration); +frames.setLifetimeHandler(&lifetime); +for (int i = 0; i < frames.getLength(); i++) + lifetime.registerFrame(frames.getFrames(i), AC_VI); +ASSERT(policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +wait(duration); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +ASSERT(ack.getQoSDataAckStatus(unrelated.front()) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +std::cout << "Block Ack scheduling isolation assertions passed.\n"; + +%contains: stdout +Block Ack scheduling isolation assertions passed. diff --git a/tests/unit/Ieee80211MultipleProtection_1.test b/tests/unit/Ieee80211MultipleProtection_1.test index 1867858da01..d777b2e0e56 100644 --- a/tests/unit/Ieee80211MultipleProtection_1.test +++ b/tests/unit/Ieee80211MultipleProtection_1.test @@ -39,11 +39,12 @@ class PredicateFrames : public InProgressFrames { int reads = 0; bool hasInProgressFrames() override { return true; } Packet *getFrameToTransmit() override { reads++; return selected; } + Packet *peekFrameToTransmit() override { reads++; return selected; } }; class PredicateAckPolicy : public OriginatorQosAckPolicy { public: bool bar = false; - bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *) const override { return bar; } + bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return bar; } }; class PredicateRtsPolicy : public IRtsPolicy { public: From f9d8cea99da8c9326ae91b838d5895cb0d88fd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 22 Oct 2026 16:04:18 +0200 Subject: [PATCH 06/16] plan: change: describe the S4 source and verification limits The current documents need the six-commit boundaries, direct protocol evidence, and exact causes for the accepted fingerprint values. This commit describes the implemented behavior and its owners. It separates inherited lifecycle support from S4. It records protocol, unit, module, and fingerprint checks with their source identities and scope limits. This documentation change preserves production behavior and baselines. No new protocol fixture or protocol reviewer document applies. The final focused contract and mechanical series checks verify the completed series. Plan: plan/pending/ieee80211-compressed-blockack-s4-corrections.md Change: plan | behavior.change | - | ieee80211-compressed-blockack-s4-corrections --- doc/src/users-guide/ch-80211.rst | 10 +- .../ieee80211-blockack-agreement-lifecycle.md | 394 ++------ ...compressed-blockack-replacement-summary.md | 492 +--------- ...ee80211-compressed-blockack-replacement.md | 861 ++---------------- ...eee80211-compressed-blockack-s4-commits.md | 104 +++ ...e80211-compressed-blockack-s4-execution.md | 107 +++ plan/pending/ieee80211-edca-data-lifetime.md | 110 +-- 7 files changed, 407 insertions(+), 1671 deletions(-) create mode 100644 plan/pending/ieee80211-compressed-blockack-s4-commits.md create mode 100644 plan/pending/ieee80211-compressed-blockack-s4-execution.md diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index f4569882f96..b526cdbf248 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -700,9 +700,13 @@ station then sets up an agreement with its peer automatically (with ADDBA frames) when it sends a QoS data frame shorter than :par:`maxBlockAckPolicyFrameLength` (default 1000 bytes). After that, the frames of that traffic class go out without an immediate acknowledgement, -and the station sends a Block Ack Request after :par:`blockAckReqThreshold` -(default 5) unacknowledged frames. Both parameters are on -``mac.hcf.originatorAckPolicy``. +and the station normally sends BAR after :par:`blockAckReqThreshold` unacknowledged frames. The default threshold is five. Both parameters are on ``mac.hcf.originatorAckPolicy``. + +The station applies the threshold separately to each peer and TID. It sends BAR below the threshold when the next eligible DATA belongs to another agreement or cannot fit the transmit window. The station also sends BAR when no eligible DATA remains. For example, a burst of four frames ends with BAR even with the default threshold of five. A negotiated buffer size of one requires BAR before the second sequence number. + +BAR can obtain channel access without a queued DATA frame. A later BAR within a TXOP must fit the applicable TXOP and TXNAV bounds with its response. If BAR cannot fit, the station retains that work for another contention. + +After the final exchange, the originator sends DELBA when its agreement has no queued DATA, in-progress DATA, or active BAR exchange. A zero inactivity timeout does not disable this transfer-completion rule. DATA that arrives after this boundary waits for DELBA completion. A later transfer can require another ADDBA exchange. If the last unresolved DATA expires, the originator uses the same teardown path. Ordinary recipient teardown releases complete units behind a receive gap before it releases agreement resources. The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and ``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size diff --git a/plan/pending/ieee80211-blockack-agreement-lifecycle.md b/plan/pending/ieee80211-blockack-agreement-lifecycle.md index 1880650dcbb..432d2b1bf42 100644 --- a/plan/pending/ieee80211-blockack-agreement-lifecycle.md +++ b/plan/pending/ieee80211-blockack-agreement-lifecycle.md @@ -1,404 +1,140 @@ -# Implementation plan: repair Block Ack agreement lifecycle +# Basic Block Ack agreement lifecycle -Original source observation revision: `feat/ieee80211-txop-single-protection` at `b06e8768b0e9c78b7a3665b9c69b0e0da9f30e41`. -Current integration source: local `master` at `633b071e213b825ce6019ff3e344ae49f52791ff`, with S1, R1, and S3a merged. -Plan revision: 2026-10-15, revision 21. -Source basis: IEEE Std 802.11-2024 and the revisions above. Source observations do not establish runtime results. +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc), [implementation status](ieee80211-compressed-blockack-replacement.md) -This plan follows [Compressed plan revision 31](ieee80211-compressed-blockack-replacement.md). The maintainer accepted the transfer-completion predicate in an earlier decision. On 14 October 2026, the maintainer requested a 100 ms ADDBA response timeout and fatal termination for active profile conflict. These decisions authorize this plan update, not implementation, recorded expectation changes, or publication. Revision 21 accounts for merged R1 and S3a under the current review request. The accepted lifecycle policy and strict individual-MPDU release requirement remain unchanged. +HCF keeps each Basic Block Ack agreement consistent with its role, peer, and traffic identifier (TID). This document describes implemented setup, DATA deferral, Action handling, teardown, and peer reset. Production Compressed selection fails initialization. No Compressed lifecycle behavior is claimed. -Lifecycle observations retain their original `b06e8768` pin and require a separate review against each selected stage parent. The integration source contains S1 from merged [PR #1320](https://github.com/inet-framework/inet/pull/1320). Original S1 runtime evidence retains its feature-head boundary; this update supplies no fresh runtime result for main. The A5 transfer-completion predicate is accepted. The A4 default is 100 ms. C2 will stop the simulation on a valid BAR that conflicts with an active agreement's profile. The [umbrella decision table](ieee80211-compressed-blockack-replacement.md#maintainer-decisions) records their status and dependent boundaries. +## Previous PRs and S4 scope -## 1. Core intended behavior +Most of this lifecycle contract is inherited from master. Merged [PR #1343](https://github.com/inet-framework/inet/pull/1343) supplies setup validation, Action dispatch, response waits, DATA deferral, and directional teardown. It also supplies recipient delivery of complete buffered units at ordinary teardown. Merged [PR #1368](https://github.com/inet-framework/inet/pull/1368) supplies explicit peer cleanup at association replacement. Merged [PR #1331](https://github.com/inet-framework/inet/pull/1331) supplies the finite DATA deadline consumed during setup and recovery. -HCF, the hybrid coordination function, will keep each Block Ack agreement consistent with its peer and traffic identifier (TID). A TID identifies a traffic class or stream. ADDBA establishes an agreement. DELBA ends an agreement. A Block acknowledgment request (BAR) requests reception status. +This S4 branch, [PR #1344](https://github.com/inet-framework/inet/pull/1344), adds the final-transfer path. HCF can contend for a due BAR without another DATA candidate. Matched feedback or lifetime disposal permits window progress. At a safe exchange boundary, HCF starts completion DELBA when first DATA was transmitted and no queued or numbered DATA remains. -The agreement handlers will accept only matching successful setup responses. HCF will preserve the negotiated first sequence number through deferred DATA extraction. Teardown will remove only the selected direction's state and prevent obsolete Actions from changing a later agreement. +S4 also isolates BAR state by peer and TID, separates control failure from DATA retry feedback, and centralizes agreement-window calculations. It does not add a second lifecycle owner or replace inherited teardown disposition. The [main scope table](ieee80211-compressed-blockack-replacement.md#previous-prs-and-s4-scope) separates the production commits. -Hypothetical example: ADDBA specifies sequence number 100, but its response is lost after recipient acceptance. HCF keeps that peer and TID's DATA deferred. It sends DELBA after setup failure before Normal Ack DATA can resume. Another peer remains eligible throughout this recovery. +For example, ordinary recipient teardown already releases complete DATA behind a gap. S4 supplies the completion trigger after the originator's final unresolved DATA expires. The recipient then uses its inherited delivery and cleanup path. The sections below describe the full contract available to that exchange. -This prerequisite retains Basic throughout the final [Compressed addition](ieee80211-compressed-blockack-replacement.md). Basic originated in IEEE Std 802.11e-2005 and is unsupported by IEEE Std 802.11-2024. The umbrella's [standards boundary](ieee80211-compressed-blockack-replacement.md#2-standard-support-and-model-boundary) supplies the historical clauses and online references. This prerequisite changes neither bitmap encoding nor profile-specific response rates. The [EDCA lifetime prerequisite](ieee80211-edca-data-lifetime.md) supplies the finite DATA deadline for deferred setup and recovery. +## 1. Core behavior -## 2. Current source and required evidence +The originator and recipient agreement handlers own separate role-specific records. HCF coordinates the exchange and DATA eligibility. ADDBA establishes an agreement; DELBA ends it. A Block acknowledgment request (BAR) requests reception status for DATA within the agreement. -[Hcf](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) dispatches duplicate ADDBA Actions and sends unexpected Actions into an active response step. The frame sequence can delete that Action without agreement dispatch. Queue disposal currently reaches management ownership through the tagged transaction path, which does not cover every Block Ack Action. +HCF defers new sequence assignment while setup or teardown makes the peer state uncertain. Another peer remains eligible. An accepted transmission retains its frame context until safe exchange completion. Logical retirement does not permit deletion of a frame that Tx still references. -The agreement policies do not validate the complete accepted response contract. For received DELBA, HCF uses the local receiver address for both roles' agreement lookups before deletion-signal emission. The [recipient handler](../../src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc) also uses that wrong address for agreement removal. The [originator handler](../../src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc) correctly uses the peer transmitter address for removal. Reorder cleanup lacks the required direction check. +Hypothetical example: the recipient accepts an ADDBA Request, but the originator receives no successful response. The originator keeps that peer/TID's DATA deferred. Response timeout starts DELBA recovery. Confirmed teardown permits surviving unsent DATA to resume with Normal Ack. -Both handlers supply absolute inactivity deadlines, but HCF schedules them as relative delays. Both handlers also select expired agreements only when the deadline equals the current time. Hypothetical example: at 5 seconds, a deadline of 7 seconds schedules an event at 12 seconds through `rescheduleAfter`. The equality check then misses that overdue agreement. The repair must schedule absolute deadlines and retire agreements whose deadlines are at or before the current time. +## 2. Current owners -AP reassociation can retain identical peer capability state. A signal that reports only changed capability values therefore cannot identify every association reset. Tx also provides no cancellation operation for a frame that it already accepted. - -These are source observations. Each implementation stage must supply a direct production reproduction for the old path it changes. The required original and corrected outcomes appear in the stage table and verification cases below. Runtime reachability and performance remain unverified until those cases run. +| Component | Responsibility | +| --- | --- | +| [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) | Coordinates eligibility, Action dispatch, timers, teardown, and transfer completion. | +| [Originator handler](../../src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc) | Owns originator agreements, dialog tokens, and transient procedures. | +| [Recipient handler](../../src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc) | Owns accepted recipient agreements and negotiated receive values. | +| [Originator data service](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc) | Previews and extracts eligible DATA with aggregation permission checks. | +| [Recipient data service](../../src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc) | Applies duplicate, reassembly, receive-state, and teardown disposition rules. | +| [Reorder owner](../../src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc) | Releases complete units in cyclic order and owns reorder records. | ## 3. Agreement behavior -The originator and recipient policies will use HCF's immutable typed profile and their existing MIB paths. HCF resolves the parameter once; policies do not reread it or infer a profile from PHY mode. Compressed setup requires local HT operation and valid peer HT state. Explicit Basic setup retains existing capability assumptions; absent HT state does not prohibit it. Basic is a historical INET profile, not current-standard operation. - -The originator agreement owner will retain the request dialog token until a response arrives. It will accept a response only for its pending peer, TID, and token, with success status, immediate policy, and positive buffer size. It will choose a new dialog token for each request to that peer and TID. - -The recipient will apply the HT requirement only to the Compressed profile. Basic acceptance still validates peer, TID, token, immediate policy, and supported parameters, without a new HT capability requirement. Its built-in policy will reject a modification request while the peer and TID have an active agreement. The existing agreement will remain unchanged after that rejection, as Clause 10.25.2 requires. A rejected delayed, unsupported, or modification request will receive an ADDBA Response with `SC_UNSPECIFIED` status. The response will copy the request dialog token and create no agreement. - -HCF will dispatch an ADDBA or DELBA Action only after the recipient data service accepts it as a nonduplicate complete management frame. The service will return that accepted Action to HCF instead of deleting it before dispatch. HCF will retain the existing immediate ACK behavior for a valid duplicate, but it will suppress duplicate agreement actions. The existing management sequence cache will identify MAC retries; no second duplicate cache is necessary. The dialog token identifies the setup transaction and does not replace MAC duplicate detection. - -During an active response wait, HCF will recognize a valid addressed Block Ack Action before it transfers frame ownership to the response step. HCF will cancel the response timer and use the existing receive-timeout operation to end the unsuccessful exchange without that Action. After sequence cleanup, HCF will pass the retained Action through normal recipient processing, including its immediate ACK and duplicate check. The existing failure path supplies recovery feedback for the interrupted exchange. HCF will not suspend and later resume that exchange. A frame accepted by Tx still follows the completion rule below; this receive rule applies only to an active receive step. - -Hypothetical example: DELBA arrives while HCF awaits Block Ack. HCF ends the unsuccessful BAR exchange, then acknowledges and dispatches DELBA through the recipient path. The agreement owner retires the selected role before deferred contention can select another DATA attempt. - -Hypothetical example: the recipient accepts ADDBA, but its request ACK and successful response are lost. A request retransmission receives its ACK without a new modification decision. The existing response transmission follows its retry procedure. If that response never arrives, the originator's response timeout starts teardown. A genuine new modification request remains subject to rejection without a change to the active agreement. - -For acceptance, the built-in recipient policy will select `min(64, maximumAllowedBufferSize)` as its response buffer size. The request size gives guidance but does not cap that choice under Clause 10.25.2. This INET policy uses the configured allocation and ignores the advisory request size. A request size of zero gives no size guidance under Clause 9.4.1.13. The built-in policy will reject a nonpositive configured maximum at initialization. The recipient will build the successful response before it creates the agreement. - -Its agreement and reorder buffer will use the accepted response size and the request starting sequence number. Hypothetical example: request size 8 and configured maximum 64 produce response size 64. Tests with a synthetic recipient will separately check other legal response choices and their effect on the originator window. - -The [completed S1 prerequisite](ieee80211-compressed-blockack-replacement.md#completed-s1-and-recorded-verification) supplies reorder record creation and window updates through Basic reception. `BlockAckReordering` creates its record on the first valid related DATA or Basic BAR from the supplied agreement values. A Basic BAR can advance that record before DATA reception. S1's fixture supplies agreements and proves no ADDBA negotiation. A2 must verify that negotiated values reach this current owner. C2 must verify Compressed integration; neither stage needs a second reorder record. - -The recipient will use its configured `blockAckTimeoutValue` as the response inactivity value. The request value is advisory. Both agreement owners will use the response value, including zero, which disables inactivity expiration. The recipient will start its deadline when it creates the agreement. The originator will start its deadline when it accepts the response. This rule replaces the current request-based recipient deadline. - -HCF will emit `blockAckAgreementAdded` when the recipient accepts and creates that active agreement. The originator will emit it only after it accepts the response. Pending and rejected records will produce no added event. If response delivery fails, the recipient agreement remains active until DELBA, inactivity expiration, or peer cleanup. - -The ACK policy will select Block Ack only for an active agreement and a frame supported by its selected profile. Compressed selection will recheck peer HT state and use Normal Ack when usable state is absent. Basic selection will preserve its historical capability and fragment behavior. The [umbrella model boundary](ieee80211-compressed-blockack-replacement.md#2-standard-support-and-model-boundary) owns the Compressed fragmentation restriction. Normal Ack cannot bypass that restriction. Local HT operation alone does not convert a Basic compatibility agreement into a Compressed agreement. - -The ADDBA request A-MSDU flag will reflect the originator's active A-MSDU aggregation policy. An accepted agreement will retain the response A-MSDU flag. Before aggregation, `OriginatorQosMacDataService` will check both negotiated A-MSDU flags for that peer and TID. If either flag denies A-MSDU, the service will extract individual DATA frames for that peer and TID. Plain DATA can then use Block Ack under the accepted agreement. - -The ACK policy will also reject any already-built A-MSDU that lacks permission; it will not use Normal Ack to bypass that restriction. Pending ADDBA and DELBA deferral will prevent new A-MSDU construction while the recipient's agreement state remains uncertain. A rejected initial request with no possible earlier agreement will release DATA under Normal Ack. A rejection of modification does not establish that the peer has no agreement. Association teardown will clear the request record. - -Keep the existing ADDBA trigger after the first eligible DATA transmission, provided more unnumbered DATA exists for that peer and TID. The agreement owner will record setup intent and defer further extraction for that peer and TID. Already numbered frames for that peer and TID will finish or drop under their existing ACK policy. Before request creation, HCF will cancel setup intent if no unexpired unnumbered DATA remains. Otherwise, HCF will query the QoS data service for the next unassigned number for that peer and TID. The data service alone will read `QoSSequenceNumberAssignment`; this query will not consume the number. - -HCF will pass the number to the agreement owner as the request starting sequence number. The fixed HCF NED composition permits this narrow QoS data-service operation without a DCF interface change. HCF will send the request after those numbered frames resolve. - -If all deferred DATA disappears during pending setup, HCF will cancel an ADDBA Request that Tx never accepted. If Tx accepted any request attempt, HCF will instead enter serialized teardown because peer agreement state remains uncertain. If a successful response leaves no DATA, HCF will immediately enter that teardown path. No final Block Ack exchange is necessary for this unused-agreement policy. Hypothetical example: one Normal Ack DATA frame with no successor causes no ADDBA. If deferred DATA expires during an existing setup, HCF cancels untouched setup or sends DELBA after possible peer acceptance. - -During setup intent, already numbered DATA remains eligible until it resolves; only further extraction is deferred. While the request or a needed DELBA remains pending, `InProgressFrames` will skip that peer and TID's DATA. HCF will supply an eligibility check from the agreement owner's current state; queue components will retain no duplicate agreement state. - -The QoS data service will scan its existing queue with `getPacket` and select the first eligible frame before it considers A-MSDU aggregation. If that frame is queue head, the service will check A-MSDU permission before it calls the prefix aggregation policy. With no agreement, the existing Normal Ack aggregation policy will apply. With an accepted agreement, both negotiated flags must permit A-MSDU. If that frame follows deferred DATA, the service will extract it individually with `removePacket`. It will preserve order within each peer and TID; other peers, TIDs, and management frames remain eligible. - -The QoS data service will expose the first eligible queue frame's receiver without extraction. For DATA, it will also report its TID and the next sequence number without consuming that number. `InProgressFrames` will expose eligible frames that it already owns without extracting another frame. HCF will check those frames first, then ask the QoS data service about its queue. Both queries will be read-only. BAR selection will use their result before any extraction or sequence assignment. - -The existing extraction path will run only after HCF selects DATA or management work instead of BAR. The queue preview and extraction will use the same eligibility check. HCF's fixed QoS data-service path needs no DCF interface change. - -HCF will not request contention for an access category with only deferred DATA and no eligible BAR. It will request access again when an accepted response, rejected response, or confirmed DELBA releases that DATA. The accepted response will release the deferred DATA, so its first extracted frame receives the request number. This eligibility path is necessary because the current extractor assigns sequence numbers before transmission. - -The originator policy will implement its existing `computeAddbaFailureTimeout` contract with a positive, finite, configurable INET timeout. The accepted default is 100 ms, as explained in [Response timeout and DATA lifetime](#response-timeout-and-data-lifetime). This value is INET policy, not an IEEE time limit. - -The agreement owner will hold one response deadline for each pending peer and TID. HCF will use one separate timer for the earliest of those deadlines. It will start the deadline after the ADDBA Request receives its Normal Ack. A response accepted before that ACK will prevent a later ACK callback from creating a response deadline. - -The owner will clear the deadline on a matched response, teardown, or peer cleanup. Invalid responses will not clear it. A request retry callback cannot replace an accepted agreement with setup failure. HCF will identify each pending Action by its retained frame identity and agreement role. A terminal callback will act only on the record that still owns that Action. - -HCF will also route queue rejection and eviction to that Action owner before the queue deletes the frame. It will use its existing queue-drop subscription and the retained identity to recognize owned Block Ack Actions. This route does not require an association-transaction tag. The owner will process one terminal outcome per Action; duplicate notifications have no further effect. +### Setup and negotiated values -For intentional invalidation, the owner will detach the obsolete Action identity before HCF removes the queued frame. The resulting queue notification cannot trigger a second recovery transition. An accepted transmission retains its separate completion context, even after that detachment. Hypothetical example: an accepted response retires a queued request retry. Disposal of that retry cannot turn the newly active agreement into teardown. +After initial eligible DATA, HCF waits for a successor before setup. HCF resolves already numbered DATA before it obtains the next sequence number without consumption. The ADDBA Request therefore names the first DATA sequence for the new agreement. DATA extraction remains deferred through the uncertain setup interval. -For an ADDBA Request, disposal before Tx accepts any attempt permits local cancellation and Normal Ack fallback. Disposal after Tx accepts an attempt requires teardown unless a matched response already resolved setup. For a successful ADDBA Response, disposal leaves the recipient agreement active until ordinary teardown, inactivity expiration, or peer cleanup. Disposal of a rejection response creates no agreement. DELBA disposal follows the failed-DELBA rule, even if the frame never transmits. Thus, queue loss cannot leave teardown-pending state without an owned exchange. +The originator accepts success only for the pending peer, TID, and nonzero dialog token. The response must select immediate policy, a positive buffer size no greater than 64, and a nonnegative inactivity value. The originator uses a new token for each request to that peer and TID. The [originator policy](../../src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc) applies these checks. -A response can arrive after request ACK timeout while that request awaits another attempt. Acceptance will remove any queued request retry before DATA resumes. A request already accepted by Tx follows the retained-context rule below. This is the relevant response-before-request-completion case; no concurrent callback inside an ordinary synchronous call is assumed. +The recipient validates request fields and rejects delayed or unsupported requests. It rejects a modification request while the agreement is active, without replacement of that agreement. A rejected request produces an unsuccessful response with the request token and creates no agreement. -An explicit rejection permits immediate Normal Ack fallback only for initial setup without possible earlier recipient state. A request discarded before Tx accepts any attempt permits the same fallback. If a transmitted request reaches its retry limit without an accepted response, HCF will queue DELBA and keep DATA deferred. It will do the same when an acknowledged request gets no response before its deadline. The agreement owner will retain one teardown-pending state for that peer and TID. HCF will ignore a late ADDBA Response after this transition. +The recipient selects `min(64, maximumAllowedBufferSize)` and its configured inactivity value. The request buffer size and timeout are advisory. The agreement uses the response capacity and inactivity value with the request starting sequence. Zero inactivity disables inactivity expiration; it does not disable DATA lifetime or completion DELBA. -HCF will treat a DELBA Normal Ack as teardown completion for this INET profile. The ACK confirms frame reception; this policy assumes that the peer processes a valid DELBA. HCF will then release unsent DATA under the ordinary no-agreement ACK and aggregation policies. If originator-role DELBA reaches its retry limit or suffers queue disposal, HCF will drop that peer and TID's currently deferred DATA. The originator owner will retain a recovery-needed record with no scheduled retry. HCF will emit `packetDroppedSignal` for each dropped frame. +Both negotiated A-MSDU flags must permit aggregation for that peer and TID. The data service otherwise extracts individual DATA. The ACK policy rejects an already-built A-MSDU without permission; Normal Ack cannot bypass that restriction. Setup and teardown deferral prevent new aggregation while peer state remains uncertain. -HCF will emit `linkBrokenSignal` for retry-limit failure through the existing recovery path. Queue disposal alone does not establish link failure and will not emit that signal. +### Action dispatch and response waits -A failed recipient-role DELBA clears its teardown-pending record without an originator recovery-needed record. A later valid BAR or Block Ack-policy DATA can trigger another recipient-role DELBA. This outcome does not discard DATA from the opposite-direction agreement. Hypothetical example: a full management queue drops originator DELBA before transmission. HCF drops the deferred batch and retains recovery-needed state, so later DATA can trigger a fresh exchange. +The recipient data service checks complete management frames and MAC duplicates before HCF dispatches accepted ADDBA or DELBA Actions. Valid duplicates retain immediate ACK behavior but produce no repeated agreement action. The existing management duplicate cache supplies this check. -New DATA in originator recovery-needed state will trigger one new DELBA exchange through the existing management retry procedure. HCF will defer that DATA until success, terminal Action failure, or DATA lifetime expiration. Frames that arrive during that exchange will share its result; they will not start parallel DELBA exchanges. Another failure drops that deferred batch and returns to recovery-needed state. Thus, transient ACK loss does not permanently prohibit future DATA. This INET policy cannot guarantee delivery while communication remains unsuccessful. +During a response wait, HCF retains an addressed Block Ack Action outside the response step. A matching ADDBA Response or directional DELBA can resolve the procedure before obsolete terminal failure feedback occurs. The frame-sequence owner then cleans the interrupted exchange without obsolete retries, batch disposal, or a second terminal outcome. An unrelated Action retains ordinary exchange failure behavior. -Hypothetical example: the peer processes DELBA, but every ACK is lost. HCF drops the deferred batch and retains only recovery-needed state. A later DATA arrival triggers a fresh DELBA exchange. Its ACK releases the new DATA under Normal Ack. No periodic retry timer or permanent peer blacklist is necessary. +HCF acknowledges the accepted frame after cleanup and avoids repeated dispatch or duplicate-cache updates. Tx-accepted outgoing Actions retain their context until safe completion. Replacement setup waits for obsolete accepted Actions to finish. -HCF will subscribe to `rateStateChangedSignal` and check current HT state for Compressed agreements. Loss of required HT state will retire the affected Compressed state. Absent HT state or a rate-only notification will not retire Basic compatibility state. At retirement, HCF will emit one `blockAckAgreementDeleted` event for each active agreement before removal. Pending, teardown, and recovery records will produce no deleted event. The same rule applies to DELBA and explicit association reset. +Hypothetical example: a successful ADDBA Response arrives during the request's final ACK wait. The originator accepts the matching response before failure notification. Cleanup starts neither request retry nor teardown for that resolved request. -At successful reassociation, management must retire old agreement state for either profile before new DATA selection. Merged R1 changes the AP commit path: it installs related HT and rate state together before notification. Identical committed state can produce no notification. The STA path clears its current association before replacement, but that alone does not establish agreement cleanup. A3b must supply an association-reset handoff for both profiles, including Basic peers without HT records. +### Directional teardown and recovery -A3b will trace the existing AP association notifications, which carry peer addresses, and the STA association and association-loss paths. Management must pass the old peer key to HCF's retirement operation before replacement or loss of that identity. Reuse existing notifications where their order and payload meet this contract. A path without a suitable notification needs an explicit synchronous retirement handoff from management. This handoff must cover Basic peers without HT records. +Received DELBA uses its transmitter address as the peer key. `Initiator=1` ends the local recipient agreement; `Initiator=0` ends the local originator agreement. The opposite role remains unchanged. Transmitted DELBA uses its receiver address as the peer key. -The handoff must preserve the public boolean MIB signal and create no synthetic HT state. Failed reassociation follows its existing disposition; capability refresh alone does not reset an agreement. +HCF retires the selected active agreement once and retains the required teardown procedure. For the originator role, surviving unsent DATA waits for confirmation. ACK reception, retry-limit failure, or queue disposal completes the DELBA exchange. Transmission completion alone does not release that procedure. -The [consumer assessment](#reassociation-consumers-and-callback-order) defines the signal observations and direct MIB readers that A3b must preserve. R1's combined commits expose coherent HT and rate state to callbacks. A3b must preserve that contract and complete agreement cleanup before new DATA selection. It must not create a temporary HT gap solely to trigger a rate notification. Trace the current callback and reset paths before a guard or another transaction mechanism becomes necessary. +Originator teardown selects discard-all for transmitted unresolved DATA, including the highest transmitted MPDU. It preserves sequence continuity for surviving unsent DATA beyond that transmission boundary. It neither reuses discarded sequence numbers nor moves allocation backwards. Inactivity teardown can therefore remove unresolved DATA before its lifetime deadline. -When either role decides to send DELBA, HCF will retire its active agreement and emit its deletion event once. The role owner will retain one teardown-pending record before HCF queues DELBA. For the originator role, HCF will defer unsent DATA until DELBA receives an ACK. A transmission-complete callback will not erase that record. ACK reception, retry-limit failure, or queue disposal completes the exchange. An unknown-agreement DELBA will use the recipient role and create no active agreement. +Confirmed teardown releases surviving unsent DATA under Normal Ack. Failed originator DELBA drops its deferred batch and leaves recovery needed. New DATA triggers one fresh DELBA recovery attempt. Failed recipient-role DELBA clears its procedure without disposal of opposite-direction DATA. -For received DELBA, the peer key is the transmitter address. `Initiator=1` ends the local recipient agreement and its reorder state. `Initiator=0` ends the local originator agreement and its transmit state. HCF will apply the role decision before either data service clears state. A transmitted DELBA uses the receiver address as its peer key. The opposite-direction agreement for the same peer and TID will remain unchanged. +A valid Basic BAR or Block Ack-policy DATA without an active recipient agreement causes recipient-role DELBA. The DATA does not reach upper delivery. The BAR creates no scoreboard or reorder record and receives no Block Ack. Malformed input does not trigger this recovery action. -A received DELBA also resolves uncertain setup or teardown for its selected role because it confirms peer teardown. HCF will remove that role's queued Actions, clear its response deadline, and release surviving unsent originator DATA under Normal Ack. It will send no DELBA in response. An Action already accepted by Tx still requires safe completion without retries; replacement setup waits for that completion. +### Recipient buffer disposition at teardown -This profile selects the discard-all option in Clause 11.5.3.4 at originator teardown. HCF will discard every transmitted, unacknowledged MPDU in that agreement, including the highest transmitted MPDU. The data service will preserve sequence continuity for unsent DATA beyond the highest transmitted number. It will not reuse a discarded number or move the counter backwards. This avoids a special retry across agreement boundaries. The same rule applies to local DELBA, received DELBA, and inactivity expiration. +Ordinary recipient teardown delivers complete buffered MSDUs or A-MSDUs before resource release. The recipient data service preserves required duplicate state. The reorder owner detaches the record and collects complete units in cyclic order. Incomplete Basic units follow teardown disposal. HCF delivers complete units before the agreement deletion event. -Inactivity expiration can therefore discard transmitted, unacknowledged DATA whose lifetime has not expired. User documentation must state that consequence alongside the discard-all choice. Hypothetical example: DATA 100 remains unacknowledged when inactivity expires, but its lifetime deadline is later. Teardown discards DATA 100 and records inactivity as the teardown cause. A5's completed-transfer predicate leaves no unresolved DATA for discard-all; its cost concerns management exchanges and later setup. Unsent DATA does not enter discard-all solely because inactivity expires. +This release across gaps is an INET policy choice. It applies to ordinary DELBA and inactivity retirement while the association remains valid. Detachment prevents repeated delivery. The opposite agreement and unrelated peers retain their state. -At recipient teardown, the recipient data service will update its duplicate cache under Clause 11.5.3.5 before it releases agreement resources. It will use the highest received sequence number in modulo-4096 order, regardless of ACK policy. The agreement owner will retain that value because scoreboard movement and delivery can remove its earlier receipt evidence. Hypothetical example: DATA 100, 101, and 102 remain unacknowledged when teardown starts. The originator drops all three; new DATA starts after 102. The recipient retains duplicate state for its highest received sequence number. +Hypothetical example: DATA 100 is absent, while complete DATA 101 and 102 remain behind that gap. The originator resolves its final outstanding DATA through lifetime expiry and sends DELBA. The recipient releases 101 and 102 in order before cleanup. Their earlier acknowledgment alone did not establish delivery. -The Block Ack owners will remove that peer's agreements and receive records on peer cleanup. The recipient data service will clear peer reorder and duplicate state for association reset. HCF will discard old transmitted DATA and clear its matching ACK and recovery records. This association reset differs from ordinary DELBA, which preserves the required recipient duplicate state. These operations will not change another peer's state. The MIB remains the capability owner; the agreement handlers remain the agreement owners. - -Before a role accepts replacement setup, its previous outgoing DELBA must reach a terminal result. The recipient will reject new ADDBA while its outgoing teardown remains pending. The originator will not start new ADDBA during its own teardown or recovery-needed state. One pending DELBA per role, peer, and TID is sufficient. Repeated unknown-agreement frames will reuse that pending teardown work. This prevents an obsolete queued DELBA from reaching a newly established agreement. - -Peer cleanup will also remove that peer's queued ADDBA Requests, Responses, and DELBA frames through the existing queue owners. HCF will invalidate matching in-progress Actions before another transmission attempt. HCF will check validity at frame selection and before it submits a frame to Tx. A frame that Tx already accepted must finish; `ITx` provides no cancellation operation. Its context must remain alive through completion, and HCF must suppress any subsequent retry or setup transition from that old work. - -Every agreement retirement will invalidate its owned ADDBA work, including ordinary DELBA, inactivity expiration, unused setup, and the first-DATA failure rule below. A response that announces the retired agreement must leave the queue before another transmission attempt. A response already accepted by Tx may finish, but its completion cannot restore the retired agreement or start another retry. Serialized teardown follows that accepted exchange before replacement setup becomes eligible. Hypothetical example: recipient inactivity expires while a successful response remains queued. HCF removes that response and sends DELBA; the originator cannot subsequently accept obsolete queued success. - -HCF will defer replacement setup until such an accepted transmission and its existing response wait reach a terminal result. It will retain the Action's ownership identity only for that interval. It will not use a generation counter as a substitute for queue removal or transmission eligibility. Hypothetical example: inactivity expiration queues recipient DELBA just before a new ADDBA arrives. The recipient rejects setup until the old DELBA completes. A later request can establish an agreement that no old DELBA can subsequently tear down. - -HCF will own one inactivity timer for both agreement roles. The recipient owner will refresh its deadline after valid matching Block Ack-policy DATA or BAR. Duplicate DATA with that policy also refreshes inactivity, before duplicate discard. Normal Ack DATA and another peer or TID will not refresh it. The originator owner will refresh its deadline after a matched Block Ack response. A zero accepted timeout will remain disabled after every refresh. - -After each agreement state change, HCF will find the earliest active absolute expiration across both owners. It will schedule that absolute time and cancel the timer when no finite deadline remains. Hypothetical example: matching BAR arrives just before expiration. The recipient sends Block Ack and moves its deadline forward by the accepted timeout. A BAR for another TID leaves the original deadline unchanged. - -At expiration, each owner will retire every agreement with a deadline at or before the current time. HCF will apply the directional disposal rule and emit one deletion event per retired active agreement. It will queue DELBA with the TIMEOUT reason code through the same serialized teardown path. It will then schedule the next future deadline. This reuses the existing HCF timer and repairs its current absolute-time error. - -HCF will create its response timer during initialization and delete it at module destruction. Peer cleanup will cancel the matching response deadline. The accepted 100 ms default bounds the response wait after an acknowledged request, not contention or teardown duration. DATA lifetime separately bounds retained DATA. The current MAC stop and crash handlers do not reset HCF timers, queues, or frame sequences. This plan makes no stop/start recovery claim; a later lifecycle change must handle those owners together. - -HCF will check for an active recipient agreement before it passes a valid BAR to the recipient data service. Without that agreement, HCF will queue recipient-role DELBA through ordinary management contention. It will create no scoreboard or reorder buffer and send no immediate ACK or Block Ack. The replacement plan owns Compressed format validation before this agreement check. - -HCF will similarly discard valid Block Ack-policy DATA without an active recipient agreement before the data service can deliver it. It will queue DELBA for that peer and TID. Malformed frames and unsupported variants will follow the invalid-frame path without protocol recovery actions. Hypothetical example: the recipient loses its agreement while the originator retains DATA 100. BAR causes DELBA, which ends the originator's obsolete agreement. DATA 100 then follows the teardown disposal rule. +Association reset uses the separate discard rule below. It does not deliver old buffered units into a replacement association. ### First DATA and final transfer -The agreement owner will retain whether its first DATA transmission occurred. Extraction alone does not satisfy that condition. If the DATA with the request number suffers terminal disposal before transmission, HCF will retire the agreement and enter serialized teardown. This rule includes lifetime expiration and terminal RTS failure that discards the protected DATA. Surviving unsent DATA will wait; sequence assignment will not reuse the discarded number. The first accepted DATA attempt can finish across its deadline under the safe-completion rule; expiration cannot cancel that accepted attempt. - -Hypothetical example: request number 100 becomes DATA 100, which expires after unsuccessful RTS attempts. DATA 101 remains available, but HCF sends DELBA before it can transmit 101. DATA 101 can later use Normal Ack; a later agreement obtains a fresh request number. Thus, 101 cannot become the first DATA transmission under the agreement that specified 100. +The originator agreement records actual DATA transmission. Extraction alone does not satisfy that condition. If the requested first DATA suffers terminal disposal before transmission, HCF retires the agreement and starts serialized teardown. Surviving unsent DATA waits; it cannot become the first transmission under the obsolete starting sequence. -Under the accepted predicate, HCF will consider a transfer complete at an exchange boundary for that agreement. The agreement must have no queued DATA, in-progress DATA, or pending BAR exchange. Clause 10.25.4 requires DELBA when the originator has no DATA and its final Block Ack exchange completes. The clause does not specify this queue predicate. HCF will retire the agreement and queue DELBA when the accepted condition holds. Zero inactivity timeout does not disable that separate requirement. +HCF completes an active transfer only after first DATA transmission, with no numbered or queued DATA for that peer and TID. Completion occurs at a safe exchange boundary. HCF retires the agreement and queues originator DELBA. Zero inactivity timeout does not suppress this path. -If lifetime expiration removes all unresolved DATA, HCF will end the agreement through the same teardown path as an INET policy. Later DATA does not cancel queued DELBA; it waits for teardown before a new transfer. Hypothetical example: the fourth DATA receives a positive final bitmap and no fifth DATA exists at that exchange boundary. HCF sends DELBA even if another burst arrives shortly afterward. This interpretation can cause repeated setup and teardown across idle gaps. Section 7 measures that cost without an assumed acceptable threshold. - -#### Transfer-completion decision - -On 14 October 2026, the maintainer explicitly accepted the proposed queue predicate above. The decision condition before A5 in S4 is satisfied. The accepted interpretation preserves conditional DELBA and can cause repeated setup and teardown across idle gaps. Measurements must still establish that cost. S4 retains its source, recipient-progress, teardown, and timing requirements. +Lifetime expiry of the final unresolved DATA can trigger the same completion policy. Later DATA does not cancel queued DELBA. Repeated bursts can therefore require new contention and setup. No general performance bound follows from that policy. ### Agreement transitions -This table summarizes the rules above and the first-DATA rule above. Each agreement owner retains its role's state for one peer and TID. The table adds no scheduler or second agreement store. Terminal Action outcomes include queue disposal. Any frame already accepted by Tx retains its exchange context until safe completion. - -| Role and current state | Event | Next state | Owned Action | DATA eligibility and disposal | -| --- | --- | --- | --- | --- | -| Originator has no agreement. | Eligible DATA transmits with more queued DATA. | Setup intent. | None exists yet. | Numbered DATA resolves; extraction for this peer and TID stops. | -| Originator has setup intent. | Numbered DATA resolves. | Request pending, or no agreement if no DATA remains. | HCF queues one request only if DATA remains. | Unnumbered DATA stays deferred during the request. | -| Originator has a pending request. | Initial rejection arrives, or untouched request suffers disposal or cancellation. | No agreement. | HCF retires the request. | Surviving DATA uses Normal Ack. | -| Originator has a pending request. | Success arrives with DATA available. | Active agreement before its first DATA transmission. | HCF retires the request retry. | DATA resumes with the request sequence number. | -| Originator has uncertain setup. | Response timeout, terminal request failure, or loss of all deferred DATA occurs. | Teardown pending. | HCF retires request work and queues DELBA. | DATA remains deferred; expired DATA leaves the queue. | -| Originator accepts unused setup. | Success arrives with no DATA. | Teardown pending. | HCF queues DELBA. | No DATA exists; later DATA waits. | -| Originator has an active agreement. | First DATA suffers terminal disposal before transmission, transfer completes, or local teardown occurs. | Teardown pending. | HCF queues DELBA. | Transmitted unacknowledged DATA follows discard-all; surviving unsent DATA waits. | -| Originator has teardown pending. | DELBA receives an ACK. | No agreement. | HCF retires DELBA. | Surviving unsent DATA uses Normal Ack. | -| Originator has teardown pending. | DELBA reaches its retry limit or suffers queue disposal. | Recovery needed. | No Action remains. | HCF drops the deferred batch. | -| Originator needs recovery. | New DATA arrives. | Teardown pending. | HCF queues one new DELBA. | The new batch waits for the exchange outcome. | -| Recipient has no agreement. | A supported request arrives. | Active agreement. | HCF queues its successful response. | Related received DATA uses the new scoreboard and reorder state. | -| Recipient has an active agreement. | Its response receives an ACK or suffers terminal failure. | Active agreement. | HCF retires the response. | Agreement reception continues until a separate teardown event. | -| Recipient has an active agreement. | Local teardown or inactivity expiration occurs. | Teardown pending. | HCF invalidates success responses and queues DELBA. | HCF releases agreement resources; later Block Ack-policy DATA cannot reach delivery. | -| Recipient has no agreement. | Valid BAR or Block Ack-policy DATA arrives. | Teardown pending. | HCF queues one DELBA. | DATA is discarded; BAR creates no receive state. | -| Recipient has teardown pending. | DELBA receives an ACK or suffers terminal failure. | No agreement. | HCF retires DELBA. | New setup can proceed; the opposite role remains unchanged. | -| Either role has a record. | A received DELBA selects that role. | No agreement. | HCF retires that role's obsolete Actions. | Directional disposal applies; surviving unsent originator DATA can resume. | -| Either role has a record. | Peer association reset occurs. | No agreement or recovery record. | HCF retires that peer's Actions. | HCF drops old transmitted DATA and clears peer receive state. | +The role owners retain setup intent, pending request, active agreement, teardown, and recovery state as separate conditions. A matching response resolves setup. Uncertain setup requires teardown before DATA resumes. Directional DELBA affects one role. Peer reset clears both roles for the selected peer before callbacks. -### Profile conflict detection and termination +The [verification record](ieee80211-compressed-blockack-s4-execution.md) identifies the current runtime selection. Source behavior outside that selection remains a source observation rather than a new test result. -A valid BAR that conflicts with an active agreement's profile will cause a fatal configuration error. The maintainer accepted this policy on 14 October 2026. It replaces recipient-role DELBA and continued operation for this specific conflict. ADDBA does not negotiate Basic versus Compressed, so repeated setup cannot correct unchanged profiles. - -HCF will classify the BAR through the production control-frame reception path. All three conditions must hold: - -- The BAR is a well-formed Basic or Compressed frame addressed to this recipient. -- The recipient has an active agreement for the BAR's peer and TID. -- The received variant differs from that agreement's resolved profile. - -At C2, HCF will raise `cRuntimeError` before scoreboard, reorder, response, or inactivity-success updates. The diagnostic will identify local and peer addresses, TID, expected profile, and received variant. HCF will not send conflict-triggered DELBA or change agreement state before the error. This policy adds no retry timer, suppression record, blacklist, or variant-negotiation field. - -Malformed frames and unknown variants remain invalid-frame cases. A valid BAR without an active agreement follows A3c's absent-agreement recovery, including its required DELBA. A response that differs from the pending BAR variant follows response validation, not this BAR-specific fatal rule. Clause 11.5.4 covers absent agreements; it does not require this active-conflict configuration error. - -Hypothetical example: two HT stations select Basic and Compressed, but accept ADDBA. The recipient can deliver DATA before it receives the first incompatible BAR. HCF then reports the mismatch and stops the simulation. The user must correct the endpoint settings and start a new run. The stopped run does not establish successful compatibility or a valid completed performance comparison. - -Detection occurs at the first qualifying BAR, not necessarily during initialization. The MAC will not inspect another module's configuration to discover the remote profile. Fatal termination stops all traffic, including unrelated peers and valid Normal Ack traffic. The accepted policy replaces the earlier requirement for continued traffic after this conflict. Compatible runs must preserve those traffic paths. +### Profile conflict detection and termination -C2 owns the classification and fatal diagnostic because it introduces both production variants. The lifecycle owner retains ordinary teardown, disposal, absent-agreement recovery, and peer-reset ownership. Tests must verify both mismatch directions and a separately initialized compatible run. No same-run recovery, in-place profile change, or new MAC stop/start support is claimed. +HCF accepts only Basic as an operational profile in this checkout. Compressed selection fails initialization. The checkout therefore supplies no end-to-end active Basic/Compressed conflict diagnostic or recovery claim. Helper admission predicates do not enable a second production profile. ## 4. Standard requirements and INET choices -Clause 10.25.2 requires the first frame under an accepted agreement to use the request sequence number. Deferred extraction is the proposed INET mechanism for that requirement. Clause 10.25.4 supplies the conditional DELBA requirement. The [transfer-completion section](#first-data-and-final-transfer) defines INET's interpretation of its trigger and separates expiration-driven teardown. +The current Basic format is a compatibility profile. IEEE Std 802.11-2024 reserves its type value. The [standards boundary](ieee80211-compressed-blockack-replacement.md#2-standard-support-and-model-boundary) separates that profile from current-standard Compressed operation. -Clause 11.5.3.4 permits one narrower alternative to discard-all. The originator can retry the highest transmitted unacknowledged MPDU outside the agreement, with its sequence number retained. It must first discard earlier unacknowledged MPDUs. It cannot retry that MPDU within a new agreement. - -Hypothetical example: DATA 100 through 102 remain unacknowledged at teardown. The proposed policy discards all three. The permitted alternative can retain only 102 for the first retry outside the agreement. Comparison with that alternative therefore concerns one additional eligible MPDU, not unrestricted retry of the batch. - -The plan retains discard-all to avoid retry across agreement boundaries. A comparison can use an isolated experimental implementation of the permitted alternative. This comparison does not add a second production policy or configuration parameter. - -| Choice | Classification and configuration | -| --- | --- | -| ADDBA response timeout | `addbaFailureTimeout` is a positive, finite NED parameter with an accepted default of 100 ms. The value is INET policy. | -| DATA lifetime | The [lifetime plan](ieee80211-edca-data-lifetime.md#3-proposed-ownership-and-behavior) owns the parameter, default, and finite disposal bound. | -| DELBA after transfer completion | Clause 10.25.4 requires DELBA under its stated condition. INET selects the queue predicate in the transfer-completion section. | -| Discard-all at teardown | This is an explicitly selected option under Clause 11.5.3.4. User documentation must explain its disposal effect. | -| Recovery after uncertain setup | HCF waits for acknowledged local DELBA or received peer DELBA for the selected role. This is INET policy. | -| Valid profile-conflict BAR | C2 raises a fatal configuration error before state updates. Clause 11.5.4 still controls absent-agreement recovery. | -| Repeated setup after profile conflict | The simulation stops at the first qualifying conflict. Corrected profiles require a new run; no same-run recovery is supported. | +The source cites IEEE Std 802.11-2024 for shared lifecycle requirements. Clause 10.25.2 supplies setup and first-sequence conditions. Clause 10.25.4 covers completion DELBA. Clause 11.5.3.4 supplies teardown disposal alternatives; this implementation selects discard-all. Clause 11.5.3.5 covers recipient duplicate-state retention. These shared rules do not establish complete conformance for Basic. ### Response timeout and DATA lifetime -The accepted defaults are 100 ms for `addbaFailureTimeout` and 0.512 seconds for DATA lifetime. The maintainer selected 100 ms on 14 October 2026 to leave more lifetime available for teardown and subsequent DATA transmission. This is an INET policy choice, not an IEEE response deadline or a measured delivery guarantee. The parameter must remain positive, finite, and configurable; no unlimited mode is included. - -The response timeout starts after request acknowledgment; DATA lifetime starts earlier at MAC acceptance. Pending setup prohibits aggregation that could restart an A-MSDU deadline. Equal timeout durations would therefore not produce equal deadlines. The response timeout bounds only the response wait, not earlier contention or later DELBA recovery. - -Hypothetical example: DATA arrives at 0 s, and the ADDBA Request receives its ACK at 0.1 s. Without a successful response, the response deadline occurs at 0.2 s and HCF queues DELBA. The DATA deadline remains 0.512 s. This leaves 312 ms for teardown and transmission, but neither operation has a delivery guarantee. A successful response at 0.15 s instead releases deferred DATA under the accepted agreement. +`addbaFailureTimeout` defaults to 100 ms after request acknowledgment. It bounds the wait for a response, not channel contention or DELBA recovery. The originator policy requires a positive finite timeout. -Prior queue delay can reverse that order. If the request receives its ACK at 0.45 s, the response deadline is 0.55 s. The original DATA expires at 0.512 s. If all deferred DATA disappears, unused-setup teardown clears the pending response deadline. Newer DATA can keep setup pending until the response timeout. +The response inactivity value controls both active roles. Each handler uses an absolute deadline. HCF schedules that absolute time and retires due or overdue agreements. Zero disables inactivity expiry. -An unlimited response wait would remove the independent setup bound. Repeated arrivals could keep deferred DATA available while each older frame expires. Setup could then remain pending without a response or another cleanup event. Finite DATA lifetime alone does not prevent that outcome. +The separate [QoS DATA lifetime](ieee80211-edca-data-lifetime.md) defaults to 0.512 seconds. Setup deferral, retries, and ACK-policy changes consume that deadline. Agreement activity cannot extend it. An accepted transmission can finish safely across its DATA deadline, but another attempt cannot start at or after that deadline. -The default decision is satisfied; A4 still requires its source review, prerequisites, and direct verification. Tests must measure request contention, response wait, DELBA completion, delivery, and disposal separately. Compare the accepted 100 ms default with a longer positive value, such as 200 ms, under identical controlled contention and response loss. Shorter values can provide additional sensitivity checks. Report unnecessary teardown after delayed responses and any resulting DATA loss. User documentation must explain these limits; document approval supplies no runtime evidence. +## 5. Queue and association integration -The accepted 100 ms default remains unchanged. No measured acceptable-loss threshold is claimed. A specific performance guarantee would require an explicit acceptance threshold; the present comparison measures costs without such a guarantee. +### Selected-extraction contract -Hypothetical controlled-contention case: the request receives its ACK at 0.1 s, and the recipient's response requires another 150 ms for channel access. With 100 ms, timeout starts DELBA recovery at 0.2 s. With 200 ms, the originator can accept a response at 0.25 s. Produce the delay through actual contention or queue residence, not direct response injection. Keep traffic, rates, channel conditions, and seed equal across the comparison. +The originator data service uses `canPullPacket()` to inspect queue service order. If the selected DATA is the advertised next frame, individual extraction uses ordinary dequeue. Otherwise, filtered `canPullPacket()` and `pullPacket()` calls select eligible DATA behind a deferred frame. The service validates every borrowed aggregation candidate before extraction. Invalid candidates cause individual extraction of the selected eligible frame, without extraction of other sources. -Record the observed response delay, teardown exchanges, DATA delivery, lifetime disposal, and management airtime. DELBA can invalidate a queued response before transmission; distinguish that outcome from an observed late response that the originator ignores. Successful DELBA releases surviving unsent DATA under Normal Ack. A new ADDBA exchange is not required before that DATA resumes, although later traffic can trigger another setup. - -## 5. Reviewable stages and dependencies - -| Stage and purpose | Owner and proposed change | Expected result and direct verification | -| --- | --- | --- | -| A1. Deliver accepted Actions exactly once. | HCF and the recipient data service repair duplicate dispatch and reception during an active response wait. Preserve immediate ACK and frame ownership. | A retried request produces one agreement action. DELBA during a response wait reaches its handler after one failed exchange outcome. | -| A2. Use the selected profile and negotiated values. | Policies validate peer, TID, token, status, policy, and parameters. Compressed also requires local and peer HT state. Reject unsupported modification without a state change. | Explicit Basic retains legacy ad hoc and non-HT setup. Compressed admission accepts eligible HT peers and refuses capability gaps. Response size, timeout, and A-MSDU permission control both roles. | -| A3a. Repair directional teardown and inactivity. | HCF, role handlers, and data services repair peer lookup, role selection, duplicate state, and absolute inactivity scheduling. The first retirement path also supplies the Action invalidation and accepted-exchange completion operations that it needs. | Both roles emit the correct deletion payload once. Removal preserves the opposite role. Overdue agreements retire without another expired deadline. Obsolete success responses cannot survive retirement as eligible work. | -| A3b. Repair association reset for both profiles. | Management supplies the peer reset to HCF before replacement. Use existing notifications or the required synchronous handoff. HCF reuses A3a's retirement operations. Trace consumers and callback order. | Reassociation clears old state, including Basic peers without HT records. Compressed state resets with identical capabilities. New DATA sees replacement state; another peer remains unchanged. Failed reassociation retains its disposition. | -| A3c. Complete Action disposal and absent-agreement recovery. | HCF and role handlers complete queue-disposal outcomes and obsolete-Action checks through the existing retirement operations. HCF checks recipient agreement state before DATA delivery or BAR dispatch. | Queue disposal produces one terminal outcome. Accepted transmissions finish safely without obsolete retries. Valid BAR without an agreement queues recipient-role DELBA without receive-state allocation. Valid Block Ack-policy DATA without an agreement is discarded and causes recipient-role DELBA. | -| A4. Preserve sequence identity through uncertain setup. | HCF, the originator handler, and the QoS data service implement setup intent, eligible extraction, response deadlines, and serialized recovery. Connect queue disposal and first-DATA expiration. | The first transmitted DATA uses the request number. Lost responses cannot release uncertain DATA. Failed DELBA disposes of its batch; later DATA can retry recovery. | -| A5. End a completed transfer. | HCF applies the accepted queue predicate after BAR-only work becomes available in the replacement plan's channel-access stage. | Test that boundary with a finite burst and zero inactivity timeout. Later DATA waits for queued teardown. Preserve the conditional DELBA requirement. | - -A3 denotes the three review units A3a, A3b, and A3c. Review them in that order, with direct reproductions and tests beside their changes. A3b and A3c reuse A3a's retirement operations. A4 depends on S2, A1, A2, and all three A3 units. A1, A2, and the A3 units do not depend on DATA lifetime. A5 belongs with the BAR channel-access integration in S4, because final completion needs the last exchange to occur. - -Before S4 publication, its owner must verify changed Basic BAR timing against S4's pinned parent. This includes the final BAR that permits A5 transfer completion. The [umbrella airtime contract](ieee80211-compressed-blockack-replacement.md#airtime-acceptance-and-admission-dependencies) owns the observations, permitted-overrun classification, and any required correction. A future C2 result cannot replace S4's publication evidence. - -The response timeout, transfer-completion predicate, and fatal profile-conflict policy are accepted. Their decision conditions are satisfied; stage prerequisites and verification remain required. S1 and R1 retain their independent review boundaries. - -These review units do not automatically define independent publication boundaries. Action invalidation and safe completion must accompany the first retirement or reset path that requires them. A3a and A3b cannot publish unsafe intermediate behavior while those operations await A3c. Hypothetical example: inactivity retires an agreement while its successful ADDBA Response remains queued. A3a must remove that obsolete response before another attempt; A3c later extends disposal and absent-agreement coverage. S3b repeats the repaired recipient tests with the accepted response buffer size after A2 establishes that negotiation result. - -S3a's [explicit Basic settings](ieee80211-compressed-blockack-replacement.md#s3a-demonstration-migration-and-fallback-inventory) are merged. The umbrella owns the selector, consumer inventory, and separate parent evidence. S3b must select a parent that contains merged S3a and R1. A2 must replace the temporary Compressed initialization rejection and revise its selector fixture. Existing topologies, PHY modes, traffic, fragmentation cases, and parameter studies remain. Basic eligibility requires no new HT capability exchange. - -The umbrella owns the [fixture inventory and VHT contract](ieee80211-compressed-blockack-replacement.md#s3a-tracked-fixture-inventory-and-vht-scope). S3b adapts initialized handlers while Basic fixtures retain explicit Basic selection. Existing `ac` infrastructure fixtures must preserve real HT capability observations and successful Basic exchanges before C2. C2 adds matched Compressed exchanges and positive receipt bits; C4 verifies the final default. - -A2 will separately verify the Compressed profile's admission boundary. Before C2, focused policy and setup tests must not claim a complete Compressed DATA/BAR exchange. Positive setup needs usable HT state; a capability gap must refuse setup before the unsupported builder runs. This boundary must not affect explicit Basic demonstrations. - -The proposed `Ac_BlockAckFallback.test` selects Compressed and simplified AP/STA management. It removes peer HT state after initialization, before DATA. Association, local HT support, and Block Ack enablement remain. No Beacon refresh restores the removed records. Require Normal Ack DATA and no ADDBA, BAR, or Block Ack throughout the measured interval. The umbrella owns its overrides. - -Hypothetical example: the existing ad hoc QoS run 1 selects Basic and has no peer HT records. It retains Basic setup and exchanges after A2. A separate Compressed case with the same capability gap uses Normal Ack. This distinction preserves legacy demonstration coverage without a current-standard claim. - -Every proposed publication boundary must preserve a Basic path that passes direct checks. Shared lifecycle repairs also require profile-specific admission and cleanup checks. A partial recovery state machine is not publishable. If stages need one atomic change, record the concrete shared invariant before combination. - -Basic remains in independently published prerequisites and the final implementation. Each stage records its parent revision and changed fingerprint causes. R1 rate publication has its own parent comparison. C2 repeats its notification integration after the lifecycle subscriber and publication both exist. No prerequisite alone claims complete Compressed support. +The queue owner retains responsibility for ownership, residence metadata, pull events, and compound accounting. This checkout defines no separate optional selected-extraction interface. The [migration guide](../../doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst) describes extension obligations. ### Reassociation consumers and callback order -The original inventory below searched `rateStateChangedSignal` and `rateStateChanged` in source, tests, configurations, and NED statistics. R1 adds production fixtures and combined MIB commits to that source. A3b must refresh the full consumer search against its selected parent. The inventory separates notification listeners from direct MIB readers; it does not establish the absence of external consumers. - -| Consumer or contract | Confirmed source behavior | A3b obligation | -| --- | --- | --- | -| Production signal listeners and NED statistics. | No current production subscription or NED statistic consumes this signal. [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) will become a new subscriber under this plan. | Repeat both searches before implementation. Verify HCF cleanup through the new callback. | -| [MIB unit-test observer](../../tests/unit/Ieee80211MibPeerTeardown_1.test) and R1 state-commit fixtures. | Test subscriptions check boolean notifications and coherent state visible from callbacks. | Preserve teardown and combined-commit assertions. Add observations for the explicit association reset, including unchanged records. | -| [QosRateSelection](../../src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc) and [RateSelection](../../src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc). | Both query `findPeerHtState` for peer-compatible mode selection. They do not subscribe to this signal. | Check final peer-compatible selection and prevent new DATA selection before reset and replacement complete. | -| [RateControlBase](../../src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc), AARF, and Onoe. | Current rate control handles mode-set changes and transmission feedback, without a subscription to this signal or direct peer HT lookup. | Verify that the reset causes no additional rate-control reset or data-rate statistic solely through the new MIB notifications. | -| [Public signal contract](../../doc/src/migration-guide/index.rst). | The boolean signal reports committed MIB changes without peer details. External consumers can subscribe. Basic peers can have no HT record. | Preserve coherent committed state. Trace a separate association-reset handoff for both profiles, including identical replacement state. | +Management calls `resetPeerAssociationContext()` before replacement of the selected peer association. The MAC passes that call to HCF's `resetPeer()`. This direct call identifies a reset even when capability values remain identical. A MIB capability-change notification cannot serve that purpose. -The merged [MIB commit operations](../../src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc) install related state before one change notification. AP successful reassociation uses that combined commit and does not require HT removal before installation. Unchanged records can produce no notification. The boolean rate signal therefore cannot identify every association reset for either profile. +Both role owners detach the peer's active agreements and transient procedures before callbacks. HCF invalidates obsolete Actions and clears transmit, acknowledgment, recovery, reorder, reassembly, duplicate, and lifetime state for the old association. Cleanup discards old buffered units without delivery. Only after cleanup does HCF emit deletion events for active agreements. Transient records produce no agreement deletion event. -Hypothetical example: an AP commits successful reassociation with unchanged HT capabilities, unchanged rate sets, and an active Compressed agreement. The MIB emits no change notification. Management must still pass the peer key through the association-reset handoff, so HCF retires the old agreement. New DATA selection occurs only after reset and replacement complete. The same reset requirement applies to Basic without an HT record. - -Before A3b implementation, trace synchronous callbacks through association reset and combined installation. Include HCF queue-disposal and channel-access calls. Add a guard only when the trace shows a reachable decision that existing control flow cannot prevent. One simulation event prevents event interleaving; it does not prevent nested callbacks. Production AP and HCF checks must establish the reset order and preserve R1's coherent notification contract. +Another peer remains unchanged. The MIB retains capability ownership and publishes coherent committed state separately. An accepted transmission retains safe completion protection through reset. ## 6. Direct verification -- Completed recipient control: Retain all eight Basic scenarios in [Ieee80211BlockAckReordering_1.test](../../tests/module/Ieee80211BlockAckReordering_1.test). Use the exact selector in the [umbrella verification section](ieee80211-compressed-blockack-replacement.md#6-verification-and-scope-limits). The fixture supplies agreements and known buffer sizes. A2 must verify negotiation through HCF without removal of these recipient assertions. C2 and C4 must verify Compressed integration separately. - -- Retained demonstrations: Repeat S3a's Basic assertions on every existing wireless hop before and after A2 and at C4. Record explicit Basic settings, peer addresses, TIDs, and positive fragment or frame receipt status. Ad hoc QoS run 1 must retain Basic without HT records; run 0 remains support-disabled. Check Compressed fallback separately. - -- Existing fixtures: Run the umbrella inventory's S3b cases through their actual handlers. Select Basic for the retained synthetic cases. Require valid capability state only for Compressed cases. Preserve header, NAV, and protection assertions. Check A-MSDU permission only after an accepted agreement, with both allowed and denied cases. Retain successful Basic setup for eligible `ac` peers through full management and real capability exchange. Record any changed failure cause in the four existing deferred protocol fixtures. - -- Absent-peer-state fallback, A2: Select Compressed in the umbrella fixture before static association and explicit peer HT removal. Require delivered Normal Ack QoS DATA with local HT support and Block Ack enablement retained. Check absent peer HT state throughout traffic at both ends of each measured hop. Require no ADDBA, BAR, or Block Ack during that interval. Repeat at C2 and C4 under the replacement plan's ownership. This test establishes fallback under the controlled gap, not a general `ac` limitation. - -- ADDBA and DATA selection: Use production HCF tests for accepted and rejected requests, including zero request size. Check the built-in response rule `min(64, maximumAllowedBufferSize)` for request sizes below and above its result. Use a synthetic recipient for other legal response sizes and check the originator window. Check the first DATA number after deferred extraction. Put deferred peer A before eligible peer B in one queue. Confirm that B sends individual DATA and A remains unnumbered. - -- Negotiated recipient window, A2: Accept an agreement with request start 100 and buffer size eight. Before any DATA reception, send a valid BAR at 104. Require record creation from the accepted values through the recipient prerequisite's reorder owner. Check reorder start 104. Receive DATA 104. Require immediate delivery. - -This check verifies negotiation integration; S1 owns the underlying recipient repair. - -- First agreement transmission: Check that A-MSDU denial prevents aggregation for the accepted peer and TID. Accept ADDBA with request number 100, then expire extracted DATA 100 before Tx accepts it while DATA 101 remains. Check teardown before 101 can transmit and absence of sequence reuse. Repeat with terminal RTS failure before DATA transmission. An accepted DATA attempt that completes across its deadline must not trigger this untransmitted-DATA rule. - -- Duplicate setup: Lose the request ACK after recipient acceptance. Retry that request before the successful response reaches the originator. Check one agreement addition and no rejection caused by that MAC retry. Test loss of the successful response through its retry limit. Test a genuine new modification request separately. Its rejection must preserve the active agreement. - -- Uncertain setup: Lose an ADDBA Response after recipient acceptance. Keep same-peer and same-TID DATA deferred until DELBA receives an ACK or DATA expires. Check the same rule after a transmitted ADDBA Request reaches its retry limit. A failed request attempt below that limit must preserve deferral. A request discarded before Tx accepts any attempt may release DATA immediately. A late response must not replace teardown state. - -- Response retry before expiry: Lose one successful ADDBA Response. Deliver its retry before both the response deadline and the deferred DATA deadline. Check acceptance and DATA release without timeout teardown. Permit channel access before expiry. Check DATA delivery. - -- Timeout and lifetime order: Suppress responses with request acknowledgment at 0.1 s after DATA acceptance at 0 s. Require response timeout at 0.2 s before the 0.512 s DATA deadline. Check serialized DELBA and DATA release only after successful teardown. In another case, delay request acknowledgment until 0.45 s. Check DATA expiration before the 0.55 s response deadline. Test both empty-setup cleanup and newer DATA that keeps the deadline reachable. Record each disposal cause. Compare the accepted default with a longer positive timeout; shorter values are optional sensitivity checks. - -- Setup completion order: After request ACK timeout, receive an accepted response before the queued request retry transmits. Check removal of that retry and absence of a new response deadline. Check that a stale request failure cannot remove the accepted agreement. Confirm that DATA already numbered before setup intent resolves before ADDBA starts. Confirm that unnumbered DATA remains deferred. - -- Reception during a response wait: Deliver valid ADDBA and DELBA Actions while HCF awaits an exchange response. Check one unsuccessful exchange outcome before recipient dispatch, immediate ACK, duplicate suppression, and safe frame ownership. Check that recipient dispatch changes the agreement before another contention attempt can select DATA. Repeat DELBA with both Initiator values. - -- Action queue disposal: Reject or evict owned ADDBA Requests, Responses, and DELBA frames. Repeat disposal before any attempt and after an earlier failed attempt. Check exactly one terminal outcome and the transition table's resulting state. A dropped successful response must leave its recipient agreement active. A dropped originator DELBA must enter recovery-needed state and dispose of its deferred batch. A dropped recipient DELBA must preserve the opposite-direction agreement. - -- Failed DELBA: Let the peer process DELBA, but lose every ACK until the retry limit. Check disposal of the current deferred batch. Check that no periodic DELBA retry occurs without new DATA. Inject later DATA and permit the fresh DELBA exchange to succeed. Confirm Normal Ack delivery without peer reset. Another failure must dispose of only the new deferred batch. - -- Profile conflict, C2: Establish an active agreement through production HCF with opposing endpoint profiles. Exercise both mismatch directions with a valid addressed BAR. Require the fatal diagnostic with both addresses, TID, expected profile, and received variant. Check no conflict-triggered DELBA, agreement mutation, or receipt, reorder, response, or inactivity-success update before termination. Verify the diagnostic cause; a generic expected failure is insufficient. - -- Persistent conflict, C2: Keep opposing settings unchanged and require termination at the first qualifying BAR, without another setup attempt. Start separate runs with matching Basic and matching Compressed settings. Check successful exchanges and Normal Ack traffic for another peer in those compatible runs. Test malformed, unknown-variant, unaddressed, and absent-agreement inputs separately to exclude false conflict errors. No traffic-continuation claim applies after fatal termination. - -- Missing agreement, A3c: Inject a valid addressed BAR without an active agreement. Check DELBA, no immediate response, and no scoreboard or reorder allocation. Inject DATA with Block Ack policy under the same condition. Check discard before delivery and recipient-role DELBA. Invalid BAR must cause neither allocation nor DELBA. Repeated valid frames must not create parallel DELBA exchanges. C2 repeats the BAR boundary after Compressed format validation exists. - -- Directional teardown: Establish both directions for one peer and TID. Test both Initiator values with local and received DELBA. Check removal of only the selected role. Teardown with unacknowledged DATA 100 through 102 must discard all three. Check that unsent DATA preserves sequence continuity beyond 102. Check recipient duplicate state after out-of-order reception and sequence wrap. - -- Deletion signals, A3a: Receive DELBA for each role with both directions active. Check one nonnull deletion payload for the selected peer, TID, and role before removal. Check removal of that agreement and preservation of the opposite role and another peer. Repeat DELBA after removal. Check that no second deletion event occurs. - -- Queued Actions: Queue DELBA after inactivity expiration, then deliver new ADDBA before DELBA transmits. Check that the recipient rejects setup until teardown completes. Establish a later agreement and check that no obsolete DELBA reaches it. Repeat peer cleanup with queued ADDBA Request, Response, and DELBA. Repeat with an Action already accepted by Tx. Check safe completion without a retry or state change from obsolete work. - -- Obsolete success response: Expire recipient inactivity before a queued successful ADDBA Response transmits. Check removal of that response, one deletion event, and serialized DELBA. Repeat after Tx accepts the response. Check safe completion without restored agreement state or another response retry. Repeat ordinary received DELBA while owned ADDBA work remains queued. - -- Association reset: Test disassociation and reassociation on both AP and station under both profiles. Include Basic peers with no HT record. Observe the explicit management reset handoff and preserved Basic state after unrelated rate notifications. Reassociate to the same AP with identical capabilities and operation values. Check removal of old agreements, duplicate state, and obsolete Action work. Preserve another peer's state. Check that failed reassociation follows the existing association disposition. Check one deletion event per active agreement and none for inactive records. - -- Reassociation consumers, A3b: Commit reassociation through production AP management with an active agreement and identical HT and rate state. Require the explicit reset handoff even when the MIB emits no change notification. Observe coherent HT and rate records in each notification callback. Instrument DATA selection and ACK-policy queries for the reset peer. Require no such production decision before reset and replacement complete. Check peer-compatible rates, preserved rate-control state, and no notification-driven data-rate statistic. Repeat with failed reassociation and no prior HT record. - -- Inactivity: Use different request and response timeout values. Check that both roles use the response value, including zero. Test expiration after nonzero simulation time with two role deadlines. Receive matching BAR just before expiration and check deadline refresh. Repeat with qualifying duplicate DATA, Normal Ack DATA, and another TID. Check one timeout DELBA and no same-time reschedule loop. - -- Inactivity disposal, A3a: Expire an agreement while transmitted DATA remains unacknowledged. Check discard-all for that agreement's transmitted DATA. Preserve unsent DATA and the opposite-direction agreement under their applicable contracts. After S2 integration, repeat with a DATA lifetime deadline in the future. Record inactivity-driven teardown disposal separately from lifetime expiry and other teardown causes. Check that A5 completion has no unresolved DATA for discard-all after A5 exists. - -- Empty setup and defaults: Send one Normal Ack DATA frame without a successor and check that no ADDBA follows. Expire all deferred DATA during setup intent, before request transmission, and after request transmission. Check local cancellation or serialized teardown according to possible peer state. Accept a response with no DATA and check immediate unused-agreement teardown, including with zero inactivity timeout. With the configured defaults, check both timeout-before-expiry and expiry-before-timeout cases through controlled request delay. Keep newer DATA available in another case to exercise the response timeout itself. - -- Final transfer: Under the accepted predicate, complete a finite burst with no queued or in-progress DATA for that agreement. Check DELBA after the final Block Ack exchange with a zero inactivity timeout. Inject DATA after DELBA enters the queue. Check that it waits for teardown rather than canceling DELBA. Expiration of the final unresolved DATA must also end the transfer. Another peer's queued DATA must not suppress this completion. - -A5's final-transfer assertions must use the accepted queue predicate at the exchange boundary. - -Use the current Basic variant for variant-independent prerequisite reproductions. Repeat the relevant cases with Compressed frames when the replacement exists. Agreement rejection and missing-agreement cases must preserve their Action and disposal results under either variant. - -Add a proposed `tests/module/Ieee80211BlockAckAgreementLifecycle_1.test` for these production paths. Partition its scenarios by A1, A2, A3a, A3b, A3c, A4, and A5. Give each review unit a precise selector. Retain prerequisite invalidation checks wherever an earlier unit consumes those operations. Confirm the actual selector syntax when the fixture exists. +Current selected module files cover Action delivery and disposal, negotiation, association reset, DATA selection, deferral, collision, lifecycle, fragment retirement, response waits, scheduling, and completion cost. The [verification record](ieee80211-compressed-blockack-s4-execution.md) gives the executed commands and counts. A fixture name alone does not establish a result for every path above. ## 7. Performance and evidence limits -Use deterministic fault injection for successful setup, lost ADDBA responses, and lost DELBA acknowledgments. Use finite bursts and traffic with idle gaps. Record the traffic definition, offered load, seed, rates, queue capacity, and relevant timeout parameters. - -Measure these results separately: - -- Measure setup deferral from the start of DATA ineligibility to release or disposal. -- Measure request contention, response wait, and DELBA completion as separate intervals. -- Measure delivered DATA, lifetime disposal, teardown disposal, and management-frame airtime. -- Separate inactivity-driven teardown disposal from other teardown causes. After S2 integration, record whether each discarded frame still had an unexpired DATA lifetime deadline. Count A5 completion separately from inactivity expiration. -- Count ADDBA and DELBA exchanges per burst, including retransmissions. -- Measure application goodput as useful bytes delivered to the destination application per unit of simulated time over a fixed measurement interval. -- Measure delivery delay for the affected TID and progress for another eligible peer or TID. -- Count the highest transmitted unacknowledged MPDU eligible for the optional retry at each teardown. - -For a performance estimate against optional final-MPDU retry, implement that alternative only in a controlled comparison. Do not infer recovered delivery from an eligible-frame count. Report the count alone when that experiment does not exist. - -For transfer completion, compare equal payload totals in continuous traffic and finite bursts separated by controlled idle gaps. Record burst size, gap duration, and the number of agreement establishments and teardowns. Attribute management airtime and DATA deferral to the observed exchanges. These measurements expose repeated setup cost; traffic-shape differences alone do not isolate the predicate's cost. - -An isolated comparison may retain agreements across idle gaps to estimate that cost, with all other inputs fixed. Such an experimental policy is not a standards exception or a second production mode. Without that experiment, report observed exchange counts, airtime, delay, and goodput without a claim about agreement-reuse performance. - -The [replacement plan's verification scope](ieee80211-compressed-blockack-replacement.md#6-verification-and-scope-limits) defines common fingerprint controls. Diagnose changed exchanges separately for each stage. No particular performance improvement or acceptable loss threshold is assumed. +Completion DELBA and a new setup can add management exchanges between bursts. Basic single protection also requires a new TXOP for unprepared DATA. Current validation supplies no general throughput, latency, or agreement-reuse cost result. MAC stop/start recovery, production Compressed exchanges, and complete NAV conformance remain outside the claim. ## 8. Standards evidence and scope limits -IEEE Std 802.11-2024 supplies the Compressed profile's normative requirements. Canonical clause identifiers use `ieee80211-2024:clause:` followed by the number. Basic is historical compatibility, unsupported by that standard. IEEE Std 802.11e-2005 Clauses 7.2.1.7, 7.2.1.8, and 9.10 introduced its format and exchange. The umbrella's [historical references](ieee80211-compressed-blockack-replacement.md#2-standard-support-and-model-boundary) link the publication record and published amendment. Shared repairs do not establish complete historical conformance. - -| Requirement | Clauses | Physical PDF pages | -| --- | --- | --- | -| Setup and accepted parameters | 10.25.2; 11.5.2.2 | 2051–2056; 2561–2562 | -| Final-transfer teardown | 10.25.4 | 2056 | -| Originator disposal and recipient duplicate state | 11.5.3.4; 11.5.3.5 | 2565–2566 | -| Inactivity and absent-agreement recovery | 11.5.4 | 2566 | - -The source review does not establish complete HT-immediate transfer support. A-MPDU, implicit BAR, and complete-cost TXOP admission remain separate work. Existing MAC stop/start limitations remain explicit; this plan does not claim their repair. - -Follow [the plan procedure](../../doc/project/guide/write-an-implementation-plan.md) and [run-the-gates.md](../../doc/project/guide/run-the-gates.md). The implementation must record exact commands, modes, selectors, and outcomes. The separate S1 record owns completed recipient verification. This plan reports no executed lifecycle test or performance result. +The clause references above identify the source requirements and current policy boundaries. The document adds no new normative interpretation or standards retrieval result. The [implementation status](ieee80211-compressed-blockack-replacement.md) states the supported profile and capability gaps. Runtime evidence remains limited to the current verification selection. diff --git a/plan/pending/ieee80211-compressed-blockack-replacement-summary.md b/plan/pending/ieee80211-compressed-blockack-replacement-summary.md index 3f5beae7321..384d192e99c 100644 --- a/plan/pending/ieee80211-compressed-blockack-replacement-summary.md +++ b/plan/pending/ieee80211-compressed-blockack-replacement-summary.md @@ -1,485 +1,67 @@ -# Compressed and retained Basic Block Ack plan summary for human review +# Basic Block Ack implementation summary -> **Kind:** reference · **Status:** snapshot 2026-10-15 · **Seal:** none · **Owns:** — · **Stands on:** [ieee80211-compressed-blockack-replacement.md](ieee80211-compressed-blockack-replacement.md) +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [implementation status](ieee80211-compressed-blockack-replacement.md) -**S1, R1, and S3a are merged; the remaining stages add Compressed Block Ack beside explicit Basic compatibility.** Management already publishes rates. Legacy demonstrations already select Basic explicitly. Basic originated in IEEE Std 802.11e-2005 and is unsupported by IEEE Std 802.11-2024. Further work covers DATA lifetime, agreement lifecycle, BAR channel access, and Compressed integration with its final default. +This checkout supports Basic Block Ack agreement setup, DATA deferral, BAR-only channel access, and transfer completion. Basic is the default profile. HCF rejects Compressed selection at initialization. Compressed frame classes and isolated admission policies do not establish production Compressed support. -This summary follows revision 31 of [ieee80211-compressed-blockack-replacement.md](ieee80211-compressed-blockack-replacement.md), dated 15 October 2026. Its integration source is local `master` at `633b071e213b825ce6019ff3e344ae49f52791ff`. It also follows lifetime revision 11 and lifecycle revision 21. The review checkout contains separate S2 source, test, and plan edits. The umbrella records its exact revision and the unavailable remote check. Earlier instructions accept the five policy decisions and strict individual-MPDU release; this request authorizes the corrections for merged prerequisites, without new implementation or publication authority. +## Previous PRs and S4 scope -This document update adds no runtime result. All five decisions are accepted; stage reviews, prerequisites, and verification remain required. The [decision table](ieee80211-compressed-blockack-replacement.md#maintainer-decisions) records all five statuses. +This summary covers the integrated source on `work/compressed-blockack-s4`, the branch for [PR #1344](https://github.com/inet-framework/inet/pull/1344). The current behavior combines inherited prerequisites with S4's transfer-completion changes. -S1 merged through [PR #1320](https://github.com/inet-framework/inet/pull/1320) on 14 October 2026. Its recorded verification belongs to original feature head `fcc06450355a52b9c8b0f8f2647e0654711bfaca`. The [completed S1 record](ieee80211-compressed-blockack-replacement.md#completed-s1-and-recorded-verification) preserves that source boundary and its scope. This document update supplies no fresh runtime verification of the merged source. - -The [completed R1 and S3a record](ieee80211-compressed-blockack-replacement.md#completed-r1-and-s3a) identifies their commits and evidence limits. R1 supplies management rate publication and combined MIB commits before notification. S3a supplies selector validation and explicit Basic consumers. Basic remains the default; explicit Compressed selection stops initialization until A2 replaces that temporary rejection. Neither stage supplies a complete Compressed exchange. - -S3a's recorded comparisons preserve thirteen demonstration outcomes, eight capability probes, and every calculated ingredient across nineteen fingerprint rows. Fifteen rows retain graphical `tyf` baseline mismatches; the fingerprint runner exits 1. Four protocol fixtures retain expected FAIL results. These historical results do not establish a fresh combined-master pass or complete protocol support. - -The [earlier source comparison](ieee80211-compressed-blockack-replacement.md#source-comparison-for-evidence-reuse) covers `6aefaef1` and `c15860de` before S1. Only three documentation files differ between those revisions; their `src`, `tests`, and `bin` trees match. That equality does not describe the source after S1. Earlier checks remain evidence only within their recorded limits. +| Source of the behavior | Contribution | +| --- | --- | +| Merged [PR #1320](https://github.com/inet-framework/inet/pull/1320) | Recipient capacity, cyclic delivery, gap release, and Basic BAR progress. | +| Merged [PR #1331](https://github.com/inet-framework/inet/pull/1331) | Finite QoS DATA lifetime and safe completion across expiry. | +| Merged [PR #1343](https://github.com/inet-framework/inet/pull/1343) | Agreement setup, Action dispatch, directional teardown, DATA deferral, and recipient teardown delivery. | +| Merged [PR #1368](https://github.com/inet-framework/inet/pull/1368) | Peer cleanup before association replacement, even when capability values remain identical. | +| This S4 branch, PR #1344 | Borrowed queue preview, BAR-only contention, peer/TID-specific feedback, window progress, completion DELBA, and prepared-successor reservations. | -The [EDCA DATA lifetime plan](ieee80211-edca-data-lifetime.md) owns DATA expiration. The [agreement lifecycle plan](ieee80211-blockack-agreement-lifecycle.md) owns setup, recovery, and teardown. The replacement plan owns recipient delivery repairs, independent BAR channel access, and final Compressed support. The sections below explain the intended combined behavior; the implementation section separates the changes. +S4 does not introduce the lifetime owner, filtered queue API, or recipient teardown policy. It uses those prerequisites to complete the final Basic exchange. Its preparation commit supplies shared window calculations before their callers. The [main reference](ieee80211-compressed-blockack-replacement.md#previous-prs-and-s4-scope) gives the detailed attribution and S4 commit boundaries. -## The intended exchange +Compressed exchanges and the Compressed default belong to separate S5 [PR #1345](https://github.com/inet-framework/inet/pull/1345). They are absent from this S4 source. The sections below describe the combined Basic behavior, not only the lines changed by S4. -Block Ack lets one response report reception status for several DATA frames. The **originator** sends DATA. The **recipient** receives DATA and reports its status. +## Agreement and DATA behavior -The agreement applies to one peer and one **traffic identifier (TID)**. A TID identifies a traffic class or stream. Three exchanges control its use: +HCF coordinates one agreement for each role, peer, and traffic identifier (TID). ADDBA establishes an agreement. A Block acknowledgment request (BAR) requests reception status. DELBA ends the agreement. The [lifecycle contract](ieee80211-blockack-agreement-lifecycle.md) defines setup validation, response waits, teardown, and peer reset. -- **ADDBA** establishes the agreement. -- **BAR**, a Block acknowledgment request, asks for reception status. -- **DELBA** ends the agreement. +Without an agreement, the originator sends eligible initial DATA with Normal Ack. When more eligible DATA exists, setup deferral preserves the requested first sequence number. Matching ADDBA success permits DATA under the accepted agreement. Uncertain setup holds surviving unsent DATA until teardown resolves the peer state. -The accepted scope uses individual DATA frames before an explicit BAR. It requires usable high throughput (**HT**) capability state at both stations. Its purpose is controlled study of Compressed bitmap, response, and recovery behavior in the current individual-frame MAC. On 14 October 2026, the maintainer accepted that [intermediate scope](ieee80211-compressed-blockack-replacement.md#purpose-of-the-individual-data-profile). The scope decision before C2 is satisfied. Complete HT-immediate transfer requires a separate A-MPDU design. +The ACK policy counts outstanding DATA separately for each peer and TID. Below the BAR threshold, it waits only when the next eligible DATA belongs to that group and fits its accepted window. HCF can contend for BAR when the queue is empty or the next candidate cannot proceed. -The response MPDU decreases from 152 bytes for Basic to 32 bytes for Compressed. That frame-length result does not establish lower exchange airtime or higher application goodput. Explicit BAR, control rates, contention, and repeated setup also affect performance. Direct exchange and airtime evidence must establish the supported result. +Hypothetical example: a window of one and a threshold of five permit one outstanding DATA frame. The next sequence lies outside the window. HCF sends BAR instead of waiting for five frames. Positive feedback permits window progress. With no queued or numbered DATA at the safe exchange boundary, HCF starts DELBA. -This hypothetical example uses TID 5, an accepted buffer size of 64, and sequence numbers near their wrap point: +The following sequence describes that supported Basic case. It omits management ACKs and ordinary channel-access delays. ```mermaid sequenceDiagram participant O as Originator participant R as Recipient - - O->>R: Earlier DATA with Normal Ack + O->>R: Initial DATA with Normal Ack R-->>O: ACK - Note over O: More DATA exists for TID 5.
Defer further sequence assignment. - - O->>R: ADDBA Request: first sequence 4094 - R-->>O: Successful ADDBA Response - Note over O,R: The agreement is active. - - O->>R: DATA 4094 - O--xR: DATA 4095 is lost - O->>R: DATA 0 - O->>R: DATA 1 - - O->>R: Compressed BAR: start 4094 - Note over R: Wait SIFS. - R-->>O: Compressed Block Ack: start 4094, bits 1 0 1 1... - - Note over O: Remove acknowledged DATA.
Retain DATA 4095 for retry. - O->>R: Retransmit DATA 4095 - O->>R: Compressed BAR - R-->>O: Compressed Block Ack confirms reception - - Note over O: No DATA remains for this agreement. + Note over O: More DATA exists. Defer extraction for setup. + O->>R: ADDBA Request + R-->>O: Successful ADDBA Response: window size one + O->>R: DATA under the agreement + Note over O: No further candidate fits the window. + O->>R: Basic BAR + R-->>O: Basic Block Ack confirms reception + Note over O: No queued or numbered DATA remains. O->>R: DELBA R-->>O: ACK ``` -SIFS is the short interval before the immediate response. The diagram omits the ordinary ACKs for ADDBA management frames. - -Sequence numbers wrap from 4095 to 0. In this example, bitmap positions represent 4094, 4095, 0, and 1, in that order. - -## Receipt status and delivery order - -The Compressed Block Ack contains 64 bits. Each positive bit confirms a complete data unit at one sequence position. The scoreboard retains Basic fragment status. The Compressed response derives one complete-unit bit per sequence position without removal of that Basic state. HCF rejects fragmented DATA only under the Compressed HT-immediate profile. Basic retains its fragment map and reassembly after final integration. - -The accepted buffer size limits the recipient window. The response still contains 64 bits; positions beyond that window contain zero. An empty scoreboard also produces zero bits. - -The BAR supplies a starting sequence number, **not a range length**. The recipient can return a different starting sequence number after it adjusts its receipt window. The originator must interpret each bit relative to the response start. - -The recipient therefore needs two separate mechanisms: - -```mermaid -flowchart TB - D["Valid DATA for an agreement"] --> S["Scoreboard
Records complete reception by sequence number"] - S --> B["Reorder buffer
Holds DATA until ordered delivery is possible"] - B --> U["Delivery to the next MAC process"] - - R["Valid BAR for the agreement"] --> V["Validate the BAR"] - V --> S - V --> B - S --> A["Compressed Block Ack
Response start plus 64 receipt bits"] -``` - -The **scoreboard** answers, “Which complete units did the recipient receive?” The **reorder buffer** answers, “Which units can the recipient deliver now?” - -Compressed delivery does not erase receipt status from the scoreboard. Scoreboard movement controls receipt retirement and preserves the preceding-position evidence required by the individual-MPDU rule below. The current delivery call erases earlier entries through `removeAckStates`, so C2 must change that contract for Compressed agreements. The Basic getter also infers positive status for absent earlier positions. C2 cannot use that inference for Compressed bits. The [recipient design](ieee80211-compressed-blockack-replacement.md#45-build-recipient-state-and-select-response-airtime) owns the complete contract and Basic compatibility. - -`BlockAckReordering` creates its reorder record on the first valid DATA or BAR for the accepted agreement. It uses the negotiated buffer size and request sequence number before it applies that frame's window update. A BAR can therefore advance the empty reorder window before any DATA reception. Hypothetical example: the request starts at 100 with buffer size eight, and the recipient receives BAR at 104 before any DATA. Subsequent DATA 104 must be eligible for immediate delivery. Invalid BAR and BAR without an agreement create no reorder record. - -S1 now releases every consecutive complete unit when DATA fills a gap. Its recipient fixture buffers DATA 101 and 102 behind absent DATA 100. Receipt of DATA 100 releases 100, 101, and 102 in order in the same operation. The recipient needs no later DATA reception or BAR. S1 also supplies record creation for Basic BAR before DATA; C2 must verify the same result through Compressed reception. - -C2 adds another release condition for individual MPDUs under Clause 10.25.6.6.2.1(a)(2)(v). An in-window unit beyond `WinStartB` can pass a gap when its interframe relation and receipt evidence satisfy that condition. HCF supplies the actual SIFS/RIFS relation after the previous MPDU, or SIFS after ACK completion. The agreement-owned scoreboard supplies receipt evidence at `WinStartR - 1`, even when it lies outside the response window. RIFS means reduced interframe space. The reorder owner uses these inputs without another timer or scheduler. - -Hypothetical example: an accepted agreement starts at 100 with size four. Normal Ack DATA 100 reaches delivery and receives ACK; the reorder start becomes 101. DATA 104 arrives one SIFS after ACK, and the scoreboard start becomes 101. Positive receipt evidence for 100 requires delivery of 104 despite absent positions 101–103. A false receipt bit or non-SIFS interval leaves 104 buffered. C2 tests the production relation and both controls. - -The receipt checks also cover delivery followed by BAR. DATA 100, 102, and 101 must reach delivery in order, with all three receipt bits retained for BAR at 100. A separate size-eight case receives only 115 and moves the scoreboard to 108–115. Only 115 has a positive bit; absent positions 108–114 remain zero. These checks expose the current deletion path and inferred positives. - -Hypothetical example: DATA 100 is absent, but DATA 101 is complete. The reorder buffer holds 101. A valid BAR at 101 can advance delivery past the gap. The scoreboard independently supplies the response bitmap. - -Both mechanisms must preserve cyclic sequence order. If one operation releases DATA 4095 and DATA 0, the recipient must deliver 4095 first. - -The recipient repair counts sequence positions, not individual fragments. Its Basic-path test supplies two fragments of one DATA unit to a one-position buffer. Both fragments must fit and permit complete-unit delivery. Final Basic compatibility retains that fragment case. It does not establish valid fragmentation under the Compressed HT-immediate profile. - -## Agreement setup and the first sequence number - -The agreement lifecycle prerequisite preserves the sequence number through setup. The first DATA transmission under an accepted agreement must use the sequence number from its ADDBA Request. - -The plan therefore defers further DATA extraction for that peer and TID during setup. Extraction matters because the current data service assigns sequence numbers at that point. - -Already numbered DATA resolves under its existing ACK policy before the request starts. Other peers and TIDs remain eligible. - -Hypothetical example: setup specifies sequence 100. Deferred DATA receives 100 after acceptance. If DATA 100 expires before transmission, DATA 101 cannot replace it as the agreement’s first transmission. The originator must end that agreement first. It must not reuse sequence 100. - -The accepted response controls the agreement parameters: - -| Parameter | Proposed behavior | -| --- | --- | -| Buffer size | The built-in recipient selects `min(64, maximumAllowedBufferSize)`. The request size is advisory. | -| Inactivity timeout | Both roles use the response value. Zero disables inactivity expiration. | -| A-MSDU permission | Both negotiated flags must permit A-MSDU construction. | -| Response identity | Acceptance requires the pending peer, TID, dialog token, success status, immediate policy, and positive buffer size. | - -An **A-MSDU** combines several service data units inside one DATA frame. If the agreement denies A-MSDU use, the originator sends individual DATA frames. A change to Normal Ack cannot bypass that restriction. - -For example, request size 8 and configured maximum 64 produce response size 64 under the proposed built-in policy. - -## Block Ack after a short burst - -The plan identifies two obstacles in the pinned source. BAR selection depends on a DATA candidate, and the default threshold is five outstanding frames. These paths can leave a finite burst of four frames without a BAR trigger. Production tests must reproduce that failure before the implementation changes those paths. - -The proposed selection rule considers outstanding DATA separately for each peer and TID. BAR can request channel access even when no DATA candidate exists. - -Hypothetical example: peer A has four outstanding frames, while peer B continuously supplies DATA. Before another DATA transmission to B, the policy selects BAR for A. - -The policy can defer BAR below the threshold only when the next DATA belongs to the same agreement and fits its transmit window. - -BAR selection and duration calculation must also leave queued DATA untouched. Neither operation may extract DATA or assign its sequence number. - -## Recovery after uncertain setup - -The agreement lifecycle prerequisite supplies recovery after uncertain setup. A lost ADDBA Response creates uncertainty. The recipient might hold an agreement that the originator never accepted. - -The originator cannot safely treat that uncertainty as an initial rejection. It must defer DATA until teardown confirms that the old agreement no longer applies. - -```mermaid -flowchart TD - P["ADDBA setup is pending"] --> S{"What result occurs?"} - - S -->|"Valid success; DATA remains"| A["Activate the agreement"] - S -->|"Initial rejection; no possible earlier agreement"| N["Resume DATA with Normal Ack"] - S -->|"Untouched request is discarded"| N - S -->|"A request could reach the peer;
setup fails or becomes unused"| T["Defer DATA and send DELBA"] - - A -->|"Transfer ends or teardown is necessary"| T - - T -->|"DELBA receives ACK,
or peer DELBA confirms teardown"| N - T -->|"Originator DELBA reaches retry limit
or suffers queue disposal"| R["Drop the deferred batch.
Retain recovery-needed state."] - - R -->|"New DATA arrives"| T -``` - -This diagram shows the originator’s main paths. Acceptance with no DATA also enters teardown. - -Hypothetical example: the recipient processes DELBA, but every ACK is lost. The originator drops the deferred batch and retains recovery-needed state. Later DATA triggers one fresh DELBA exchange. A successful exchange releases that new DATA under Normal Ack. - -The plan adds no periodic DELBA retry. DATA expiration alone does not prove that the peer cleared its agreement. - -## The three time limits - -| Limit | Purpose | Rule and decision status | -| --- | --- | --- | -| ADDBA response timeout | Bounds the wait for a response after the request receives its ACK. | The accepted `addbaFailureTimeout` default is 100 ms; the parameter remains positive, finite, and configurable. | -| Agreement inactivity timeout | Ends an agreement without qualifying activity. | Both roles use the accepted response value. Zero disables this timeout. | -| DATA lifetime | Bounds retention of DATA despite retries or setup delays. | The maintainer accepted the 0.512-second default per access function. The [lifetime decision](ieee80211-edca-data-lifetime.md#lifetime-compatibility-decision) before L1 is satisfied. | - -The accepted 100 ms ADDBA response timeout is an INET policy choice. The DATA lifetime matches the IEEE 802.11-2024 MIB defaults: 500 TU, or 512 milliseconds, for AP and non-AP stations. - -The standards basis does not establish the compatibility cost in current simulations. The maintainer accepted the 0.512-second lifetime default on 14 October 2026. S2 must still measure expiry with Block Ack disabled. Congestion alone does not prove that retained DATA reaches that age. An opt-in or unlimited mode needs a separate design and a disposal bound for Block Ack recovery. - -DATA lifetime starts when HCF accepts DATA from above, before queue insertion. HCF is the MAC component that coordinates these exchanges. Queue residence and setup deferral therefore consume lifetime. Retransmission does not restart the deadline. - -The lifetime owner tracks local frame identity, so equal sequence numbers at different peers cannot share a deadline. Fragments retain their source deadline. Each MSDU addition restarts the aggregate timer, as the [lifetime plan](ieee80211-edca-data-lifetime.md#3-proposed-ownership-and-behavior) specifies. The current builder adds all inputs at one simulation time. Pending setup prohibits that aggregation. - -Hypothetical example: an MSDU arrives at 0 seconds and joins an A-MSDU at 0.4 seconds under Normal Ack. This example has no active Block Ack agreement and no pending setup or teardown for that peer and TID. The default lifetime places the aggregate deadline at 0.912 seconds. The original 0.512-second deadline must not discard the aggregate. The lifetime tests must also prove that aggregation cannot revive an already expired input. - -Hypothetical example: DATA arrives at 0 seconds, and ADDBA Request acknowledgment occurs at 0.1 seconds. Without a successful response, HCF starts DELBA recovery at 0.2 seconds. DATA expires at 0.512 seconds, so 312 ms remains for teardown and transmission. A successful response at 0.15 seconds instead releases DATA under the agreement. - -The [lifecycle timeout contract](ieee80211-blockack-agreement-lifecycle.md#response-timeout-and-data-lifetime) records the accepted 100 ms default. Earlier queue delay and later DELBA recovery still consume DATA lifetime. Request acknowledgment at 0.45 seconds would place the response deadline at 0.55 seconds, after the original DATA expires. Tests must cover both orders; the selected default does not guarantee delivery. - -The lifecycle tests compare 100 ms with a longer positive timeout under identical controlled contention. They measure delayed-response teardown, delivery, disposal, and management airtime. They also distinguish a canceled queued response from a late received response. Successful DELBA permits surviving DATA to resume under Normal Ack without mandatory new setup. The accepted default remains 100 ms; no measured acceptable-loss threshold is claimed. - -No new DATA transmission may start after its deadline. An accepted transmission can finish safely after that deadline. A successful response can still acknowledge it, but expiration prohibits another attempt. - -The lifetime prerequisite applies to all QoS DATA, with either value of `isBlockAckSupported`. It includes Normal Ack DATA without an agreement. DCF and complete management-frame lifetime remain outside its scope. - -EDCA is the channel-access method for QoS traffic. An EDCAF serves one access category. Its positive, finite `msduLifetime` parameter controls DATA lifetime. This change can affect existing EDCA simulations under congestion even when Block Ack support is disabled. A finite value supplies a deadline, not a guarantee of prompt recovery or expiry within the run. - -The [lifetime plan](ieee80211-edca-data-lifetime.md#3-proposed-ownership-and-behavior) permits a larger finite value for compatibility studies. Hypothetical example: a 100-second lifetime prevents lifetime expiry for newly accepted DATA during a 10-second simulation. It does not promise unchanged fingerprints or complete legacy behavior. Earlier success, teardown, or other disposal can still resolve DATA. A recovery path that relies on lifetime expiry inherits the larger delay; the plan adds no unlimited mode. - -Hypothetical example: HCF accepts Normal Ack DATA at 0 seconds, but contention prevents transmission before 0.7 seconds. With the default lifetime, HCF discards the DATA at 0.512 seconds under either switch value. An explicit one-second setting still permits an attempt at 0.7 seconds. - -Before S2 completes, it requires [measured expiry under controlled congestion](ieee80211-edca-data-lifetime.md#5-verification-and-evidence) with Block Ack disabled. A selected QoS row without expiry cannot satisfy that requirement. If the common rows do not reach the boundary, the lifetime owner must add a controlled congestion variant. Evidence must separate lifetime disposal from queue-capacity loss and retry-limit loss. - -The response timeout does not bound earlier contention or later DELBA recovery. The lifecycle plan requires separate measurements for these intervals, delivered DATA, and disposal. The 100 ms default does not bound the complete setup and recovery procedure. - -## Response validation and retry - -The originator checks the response type, addresses, TID, fragment field, bitmap size, and frame length before it reports exchange success. - -It applies bitmap results only within the active agreement’s transmit window: - -- Positive bits can acknowledge covered, transmitted DATA, even if those frames did not await this particular BAR. -- Zero bits leave DATA unacknowledged and release the relevant frames for retry. -- DATA below the response start remains unacknowledged. -- A response without overlap, or a timeout, releases requested DATA for retransmission. - -The profile uses DATA retransmission after a failed exchange. It does not repeatedly send BAR alone to poll for status. - -Individual DATA retransmission preserves its sequence number and sets Retry to one under Clause 9.2.4.1.6. Zero bits and BAR timeout do not count as synthetic Normal Ack retry failures. DATA lifetime supplies the disposal bound; successful Block Ack responses cannot extend it. - -For example, a response for another TID cannot acknowledge DATA from the pending exchange. A malformed matching response cannot refresh inactivity or report success. - -## Teardown and obsolete work - -The two directions can hold separate agreements for the same peer and TID. DELBA must end only the selected direction. - -The [source assessment](ieee80211-blockack-agreement-lifecycle.md#2-current-source-and-required-evidence) distinguishes deletion notifications from agreement removal. HCF uses the wrong peer address for both roles' deletion-signal lookups. The recipient handler also uses the wrong address for removal, while the originator handler uses the correct transmitter address. A3a tests both signal payloads and agreement removal, with the opposite role preserved. - -At originator teardown, the proposed **discard-all policy** removes every transmitted, unacknowledged frame in that agreement. Unsent DATA retains sequence continuity. Ordinary recipient teardown preserves the required duplicate state. - -Hypothetical example: DATA 100 through 102 remain unacknowledged at teardown. The originator discards all three. New DATA uses sequence numbers after 102. - -Inactivity expiration also applies discard-all to transmitted, unacknowledged DATA, even when its lifetime deadline is later. For example, DATA 100 can remain unacknowledged when agreement inactivity expires before DATA lifetime. Teardown then discards it and records inactivity as the cause. User documentation must explain this consequence; measurements separate it from lifetime expiry and other teardown causes. A5 completion leaves no unresolved DATA for discard-all. Unsent DATA is outside this discard-all rule. - -Clause 11.5.3.4 permits a narrower alternative: retry only the highest transmitted unacknowledged frame outside the agreement, with its sequence number retained. Earlier unacknowledged frames must still be discarded. That alternative cannot retry the retained frame within a new agreement. In the example above, only DATA 102 qualifies. - -The proposed policy avoids this retry across agreement boundaries. A performance estimate requires an isolated implementation of the alternative for a controlled comparison. An eligible-frame count alone does not prove recoverable delivery. The comparison adds no second production policy or configuration switch. - -Clause 10.25.4 requires DELBA when no DATA remains after the final Block Ack exchange. Zero inactivity timeout does not disable this separate requirement. The accepted INET predicate checks for no queued DATA, in-progress DATA, or pending BAR exchange for that agreement at an exchange boundary. The clause does not prescribe this queue predicate. - -Hypothetical example: a burst completes and no DATA remains for its agreement before another burst arrives. The accepted interpretation queues DELBA during that gap. Later DATA waits for teardown and can incur another setup exchange. The [transfer-completion record](ieee80211-blockack-agreement-lifecycle.md#transfer-completion-decision) states the maintainer's acceptance of this predicate. The lifecycle plan must still measure its cost separately from inactivity expiration. - -Several event cases need explicit treatment: - -- Duplicate ADDBA frames receive the ordinary ACK but do not repeat agreement actions. -- An addressed ADDBA or DELBA during a response wait first ends that unsuccessful exchange, then enters normal recipient processing. -- Queue disposal produces one terminal outcome for each owned management Action. -- Obsolete queued Actions leave the queue before another attempt. -- An Action already accepted by Tx finishes safely, without an obsolete retry or agreement change. -- Association reset clears that peer’s agreement state, even when replacement capabilities are identical. - -A valid BAR without an agreement causes DELBA, without an immediate ACK or Block Ack. DATA with Block Ack policy under the same condition is discarded and causes DELBA. Malformed frames create neither receive state nor recovery exchanges. - -The [reassociation consumer assessment](ieee80211-blockack-agreement-lifecycle.md#reassociation-consumers-and-callback-order) found no current production subscriber or NED statistic for `rateStateChanged`. A unit-test observer subscribes, and HCF will become a new production subscriber. Both rate-selection components read peer HT state directly. The public signal contract also permits external listeners. - -Hypothetical example: a Compressed peer's successful AP reassociation retains identical HT capabilities and rate sets. R1's combined MIB commit can emit no rate notification. A3b must explicitly retire the old agreement before new DATA selection. The handoff must also cover Basic peers without HT records. A3b must preserve coherent callback state and verify final peer-compatible rates and rate statistics. - -## Frame format and compatibility - -HCF resolves `blockAckVariant` once into an immutable typed profile. Merged S3a keeps that value private and exposes no profile query. A2 and C2 must pass the value to their consumers through the smallest required interface. Compressed is the final default; legacy consumers retain explicit Basic selection. Policies must not infer the profile from PHY mode. The serializer decodes the actual wire type independently, without a variant registry or second agreement store. - -IEEE Std 802.11e-2005 introduced the mechanism and the frame format later called Basic Block Ack. Clauses 7.2.1.7 and 7.2.1.8 define BAR and Block Ack, with a 128-octet fragment bitmap. Clause 9.10 defines their operation. Direct inspection of the local PDF confirms these clauses, their frame figures, and the historical page references. The [umbrella source notes](ieee80211-compressed-blockack-replacement.md#appendix-b-standards-source-notes) record the filename, checksum, pages, and lack of corpus indexing. The [IEEE publication record](https://standards.ieee.org/ieee/802.11e/3131/) identifies the amendment. - -IEEE Std 802.11-2024 Tables 9-36 and 9-37 reserve type 0 and identify type 2 as Compressed. INET retains Basic for historical compatibility; that current standard does not support it. Existing INET studies can use later PHY modes and existing Basic response behavior. Retention does not claim complete IEEE Std 802.11e-2005 conformance. - -Keep Basic classes, serializers, builders, fillers, and fragment bitmaps. Decode type 0 as Basic and type 2 as Compressed. Other unsupported variants follow the invalid-frame path. A response must match the pending BAR variant before any success or receipt-state update. - -The old nine-bit `reserved` field overlaps the upper two bits of the complete Type field. C1 preserves Basic field round trips only when they encode Type 0. It rejects a Basic value that encodes another Type before serialization. The common fields remain available to existing callers. Figures 21b and 21e of the 2005 amendment reserve B0–B11. Retained ACK-policy fields are INET compatibility fields, not semantics from those figures. - -Both Compressed serializer producers already exist. C1 adapts their old ACK-policy and reserved-field expectations with its serializer change. Compressed writes zero in B0 and B5–B11; received values in those reserved positions are ignored. B3 and B4 remain Type bits and cannot use that ignore rule. C1 verifies both complete inventories; C4 adds remaining coverage. - -ADDBA gains no variant negotiation field. Both endpoints must select the same profile. A valid addressed BAR that conflicts with an active peer/TID agreement causes a fatal configuration error before state updates. HCF reports both addresses, TID, expected profile, and received variant. Malformed input, unknown variants, and absent agreements retain their separate validation or recovery paths. - -The accepted [profile-conflict policy](ieee80211-blockack-agreement-lifecycle.md#profile-conflict-detection-and-termination) stops the entire simulation, including unrelated traffic. It replaces conflict-triggered DELBA and continued operation after mismatch. The user must correct endpoint profiles and start a new run. DATA can reach the recipient before conflict detection; the stopped run does not establish successful compatibility or a completed performance comparison. - -Basic responses remain 152 bytes with FCS. The Compressed header chunk is 28 bytes; its FCS makes the MPDU 32 bytes. Timeouts, durations, and protection use the actual variant. Retain Basic response-mode assertions as compatibility evidence. Add non-HT Compressed cases beside them. - -Compressed responses use qualifying basic or mandatory rates. Non-HT BAR supplies the rate bound, modulation class, and preamble type. HT or VHT BAR supplies its non-HT reference rate and an OFDM or ERP-OFDM response. Legal alternate rates preserve duration; invalid overrides fail clearly. - -Originated Compressed BAR uses a non-HT PPDU. Initial BAR uses a BSS basic rate, or a mandatory rate when that set is empty. Later BAR uses the known peer operational set. Unknown peer state uses the basic or mandatory fallback. Unknown BSS state uses the documented INET mandatory-rate fallback. - -The merged [R1 stage](ieee80211-compressed-blockack-replacement.md#r1-publish-production-rate-state) publishes rate state through existing MIB operations. Full management commits advertised or accepted local, BSS, and peer rates. Simplified management publishes equivalent static state. Associated-AP Beacons refresh records; association loss clears them. Unknown sets remain distinct from known empty sets. - -Direct MIB injection does not prove production publication. R1 has its own parent comparison and production fixture. C2 consumes the completed state and repeats its lifecycle-subscriber interaction. - -HCF will classify initial and later Compressed BAR from actual frame-sequence progress before mode selection. The current initiator predicate always returns false and cannot supply that classification. Fresh channel access requires initial classification again. Retain unrelated RTS/CTS and ACK behavior. - -Rate-only notifications must preserve unchanged agreements in either profile. Missing HT state cannot retire Basic compatibility. Management must still notify HCF of real association reset for Basic peers without HT records. Lifecycle A3b owns that handoff and its callback-order checks. - -Compressed requires usable local and peer HT state; its capability-gap cases use Normal Ack. Explicit Basic retains existing ad hoc and non-HT capability assumptions. It requires no new peer HT records or IBSS capability implementation. - -Clause 10.2.7 prohibits fragmentation under the Compressed HT-immediate agreement, regardless of DATA ACK policy. Basic preserves historical fragment handling. An HT PHY mode alone does not select the Compressed profile. - -The [legacy inventory](ieee80211-compressed-blockack-replacement.md#s3a-demonstration-migration-and-fallback-inventory) preserves the Block Ack and TXOP ad hoc networks. Both QoS examples retain their modes, topology, and traffic. Preserve S3a's explicit Basic settings in the existing scopes. - -Retain `Fragmentation`, `HCFfragblockack`, `PacketSizeHCFfragblockack`, `TxPowerHCFfragblockack`, `ThresholdHCFfragblockack`, and `OnlyPacketSizeHCFfragblockack`. Keep their analysis inputs, result filters, plot series, fingerprint rows, and six speed records. Other Basic showcase records also remain. - -Hypothetical example: the Block Ack `Fragmentation` configuration selects Basic at both ad hoc hosts. It still reports fragments through its Basic bitmap. A valid Compressed agreement transmits the same MSDU without fragmentation. These profiles need different expectations. - -At each relevant description or analysis caption, state Basic's historical origin and its absence from IEEE Std 802.11-2024. Keep existing images and results when evidence still supports them. Shared repairs can change results despite unchanged topology and traffic. Regenerate only affected material after measurement. - -Every legacy demonstration must retain ADDBA, Basic BAR, Basic responses, and positive receipt status. Check fragments in the fragmentation cases. Ad hoc QoS run 1 retains Basic; run 0 remains support-disabled. Check Compressed capability-gap fallback separately. - -Infrastructure protocol fixtures supply Compressed coverage through real capability exchange. `N_CompressedBa` must require Compressed and positive bits. `Ac_BlockAck` remains positive coverage for eligible `ac` peers. The absent-peer-state fallback fixture selects Compressed and static management without Beacon refresh. - -Retain Basic NAV, multiple-protection, header, and serializer assertions. Add Compressed cases with valid HT state. Retain 152-byte Basic response-mode cases beside new 32-byte Compressed cases. Preserve directional queries, asymmetric state, peer isolation, and ACK/CTS controls. - -The four deferred protocol obligations remain distinct from successful exchanges. Explicit Basic retains `WifiQosDelayedBa` and its missing delayed behavior. A separate fixture verifies Compressed initialization rejection for delayed policy. This revision adds no new Basic initialization guard. - -Migration guidance explains the default, explicit Basic setting, and added interface obligations. HCF logs the resolved profile when support is enabled. If Compressed lacks usable local HT support, it emits one initialization warning for that interface. Basic and disabled support produce no irrelevant warning. - -That local warning cannot detect HT ad hoc management without peer HT records. The existing policy decision log must identify the capability-gap refusal. C4's external-style fixture omits the selector and tests legacy modes plus HT ad hoc operation. It must prove Normal Ack fallback and the applicable diagnostics. Explicit Basic must restore Basic exchanges. The default remains independent of the mode set. - -## Implementation order and verification - -S1, R1, and S3a are merged. The remaining stages are S2, S3b, S4, and S5. S3b must use a parent that contains R1 and S3a. A4 requires S2; earlier lifecycle units can proceed independently of S2. The arrows below show required dependencies: - -```mermaid -flowchart TD - S1["S1: Merged
Basic capacity, cyclic delivery, and BAR before DATA"] - S2["S2: DATA lifetime
All QoS DATA"] -->|"Required by A4"| S3b["S3b: Agreement lifecycle
Action, negotiation, teardown, and setup repairs"] - S3a["S3a: Merged
Explicit Basic settings and profile evidence"] -->|"Retain Basic checks and revise temporary rejection"| S3b - S3b -->|"Supplies teardown"| S4["S4: Independent BAR work
Channel access and final-transfer DELBA"] - S1 -->|"Supplies recipient progress"| S4 - S4 --> C1 - R1["R1: Merged
Management rates and combined MIB commits"] -->|"Committed rates and subscriber checks"| C2 - R1 -->|"Preserve coherent commits and add explicit reset"| S3b - subgraph S5["S5: Compressed addition beside retained Basic"] - C1["C1: Wire formats"] --> C2["C2: Complete exchanges and timing"] - C2 --> C3["C3: Response and timing verification"] - C3 --> C4["C4: Default activation and migration diagnostics"] - end -``` - -S2 implements the lifetime prerequisite. S3a already records explicit Basic settings and legacy exchange evidence against its original parent. S3b uses a parent with merged S3a and R1 and implements A1, A2, A3a through A3c, and A4. These units repair Action dispatch, negotiated values, safe teardown, and uncertain setup. A3c owns recovery for valid BAR and Block Ack-policy DATA without an agreement. A4 requires S2 and the earlier lifecycle repairs; A1, A2, and A3a through A3c do not require S2. - -The lifecycle plan separates A3 into [three review units](ieee80211-blockack-agreement-lifecycle.md#5-reviewable-stages-and-dependencies): - -| Review unit | Responsibility | -| --- | --- | -| A3a | Directional teardown, deletion signals, duplicate state, and inactivity. | -| A3b | Association reset, the AP management change, and consumer and callback verification. | -| A3c | Complete Action disposal and absent-agreement recovery. | - -The split does not establish independent publication boundaries. Action invalidation and safe completion must accompany the first retirement or reset path that needs them. Hypothetical example: A3a retires an agreement while a successful ADDBA Response remains queued. A3a must invalidate that response before another attempt; it cannot defer that protection to a later published A3c change. - -C2 must implement Clause 10.25.6.6.2.1(a)(2)(v), including its surrounding agreement, station, sequence-window, receipt-bit, and timing conditions. The [history and timing design](ieee80211-compressed-blockack-replacement.md#interframe-history-and-timing-design) is implementation work within that requirement. Its owner must resolve timestamp mapping, event observation, stored state, and invalidation before the affected implementation is complete. These details require source and standards evidence, not another policy choice or an optional omission of the rule. - -If PHY observations cannot establish the timing relation, the C2 owner records the gap and proposes the smallest correction. The code reviewer checks that correction against the standard and supported PHY model. A model approximation or reduced scope requires an explicit maintainer-approved plan revision with stated compliance limits. No such approximation is approved here. - -Local experimental code may support that investigation. The affected release path cannot be published until its design review and direct verification pass. Independent work may continue if it neither exposes nor depends on that incomplete path. No runtime switch is required solely to disable an unfinished rule. - -Tests must distinguish receipt-bit and timing conditions through separate positive and negative cases. They must include propagation delay, intervening frames, and cleanup. The implementation must not add unsupported same-peer, address, or TID restrictions. The rule can change Compressed delivery timing; Basic retains S1 behavior and needs separate evidence for any changed fingerprint. - -The [stage conditions](ieee80211-compressed-blockack-replacement.md#stage-conditions) distinguish start, continuation, completion, and publication. The lifetime review precedes S2 implementation. S2 reproduces absent queue expiration before the production change and demonstrates corrected expiry before completion. The lifecycle review and completed S3a preparation precede S3b implementation, including A1. The umbrella review does not establish either prerequisite plan's correctness or completed verification. - -The [maintainer decision table](ieee80211-compressed-blockack-replacement.md#maintainer-decisions) records all five accepted choices: - -- The maintainer accepted the 0.512-second lifetime default for S2/L1, with Block Ack enabled or disabled. -- The accepted 100 ms response timeout satisfies the A4 decision condition. -- The maintainer accepted the queue predicate for A5 in S4. -- The maintainer accepted the individual-DATA purpose and scope for C2. -- The accepted fatal configuration error satisfies C2's profile-conflict decision condition. - -S1, R1, and S3a retain their separate completed scope and recorded verification. The remaining stages are S2, S3b, S4, and S5. The explicit decision approvals select all five policy outcomes. They establish no completed implementation or verification of those remaining stages. - -Other stages follow their applicable review and prerequisites. S3b requires a parent that contains merged S3a and R1. A2 must revise S3a's temporary Compressed rejection and its selector fixture. `Ac_BlockAck` remains a positive requirement and adds no deferral approval gate. - -S1 corrects recipient delivery and capacity through Basic tests with supplied agreements and known buffer sizes. Its eight scenarios include complete gap release and BAR before DATA. Retain all eight as Basic regression checks through later stages. S3b/A2 must verify that negotiation supplies the accepted size and sequence number to the repaired recipient. C2 and C4 must verify Compressed integration separately. S1 does not establish the final Compressed bitmap representation. - -Before S4 starts, its owner confirms that S3b supplies teardown and S1 supplies recipient progress. S4 adds BAR work without DATA extraction and integrates A5 with the accepted transfer-completion predicate. S4 compares changed Basic BAR timing with its pinned parent before publication. Any new unlawful overrun requires resolution at that boundary, without a future C2 or C3 result. S4 preserves Basic fragment rules. S5 retains them beside the Compressed restriction. - -S5 consists of C1 through C4 after its required S1–S4 contracts. R1 already satisfies the publication prerequisite. C2 repeats subscriber integration and supplies actual BAR classification and Compressed response calculations. Those calculations cover response-rate selection, override validation, BAR timeout, recipient duration, and protection airtime. C2 records the selected modes, first airtime result, and admission decision before C3 or C4 proceeds. C3 verifies supported response modes and exchange boundaries; it supplies no missing runtime operation required by C2. - -The first C2 timing result is not a prerequisite for S5 to start. Each step must build: - -| Step | Change and required result | -| --- | --- | -| C1 | Define the 28-byte Compressed header chunk and complete Type decoding beside Basic. Retain Basic round trips that encode Type 0. Adapt existing Compressed producers and field expectations with the serializer change. | -| C2 | Consume R1 rates and add complete Compressed exchanges without loss of Basic fragment state. Preserve receipt truth after delivery and add the individual-MPDU interframe release condition. Supply actual BAR classification, legal control modes, durations, and profile validation. Enforce the Compressed fragmentation boundary. Record the first Compressed timing result before C3 or C4. | -| C3 | Verify initial and later BAR rates, primary and legal alternate responses, mandatory-rate fallback, and HT/VHT reference rates. Preserve directional response queries and ACK/CTS controls. Repeat timing checks across supported modes and exchange boundaries, with permitted-overrun conditions explicit. | -| C4 | Activate the final Compressed default after runtime and timing checks. Add migration diagnostics and the external-style fixture. Retain explicit Basic consumers and all legacy result rows. Add Compressed-only delayed-policy rejection. | - -Basic remains through every prerequisite and the final implementation. Each publication boundary must preserve a Basic path that passes direct checks. A partial recovery state machine is not publishable. S3a temporarily retains the Basic default; C4 activates the requested Compressed default after C2 and C3. - -Basic fragment scenarios remain after C4. Compressed fragment input must cause no receipt-state update, positive bit, or delivery. Ordinary Normal Ack fragmentation remains outside Compressed agreements. Timing evidence and any required admission correction still control S5 completion. - -A wire-only change cannot establish the required exchange behavior. Hypothetical example: an empty receive record must produce zero Compressed bits. A correct type 2 header cannot compensate for false positive receipt status. - -## Verification and performance evidence - -Verification combines both wire formats, production assertions, the full fixture inventory, and all 19 selected fingerprint rows through C4. Explicit Basic preserves the legacy scenarios. Shared repairs can still change results. Production assertions establish behavior; fingerprints expose regression effects. - -The current [Ieee80211BlockAckReordering_1.test](../../tests/module/Ieee80211BlockAckReordering_1.test) supplies the completed S1 recipient checks. Its default selects scenarios 0 through 7 with seed 0. The [umbrella verification section](ieee80211-compressed-blockack-replacement.md#6-verification-and-scope-limits) gives its exact module selector with the current NAV fixture. These checks supply no ADDBA negotiation or Compressed exchange evidence. - -The planned focused fixtures separate lifetime, agreement lifecycle, and Compressed integration. They include `Ieee80211EdcaDataLifetime_1.test`, `Ieee80211BlockAckAgreementLifecycle_1.test`, `Ieee80211CompressedBlockAck_1.test`, and `Ieee80211HcfCompressedBlockAck_1.test`. The current `N_CompressedBa.test` must assert Compressed frames and positive receipt bits. These later checks are proposed, with no completed results. - -R1's existing production fixtures compare committed MIB rates with real advertisements without direct rate injection. Their [evidence record](../../doc/project/evidence/model/wifi/r1-rate-publication.md) covers publication, callback state, cleanup, membership selectors, and nonfinite inputs. Retain `Ieee80211MgmtRatePublication_1.test` and `Ieee80211MgmtStateCommit_1.test` after affected changes. C2 must repeat subscriber effects after S3b exists. HCF checks must distinguish initial and later BAR rates, including initial classification after fresh channel access. RTS/CTS and ACK controls remain. - -The protocol selection includes eight original fixtures and the proposed absent-peer-state `ac` fallback fixture after it exists. The original module selection includes four fixtures that can enable Block Ack, five support-disabled controls, and the separate S1 recipient fixture. Add S3a's selector and Basic observation fixtures, plus R1's existing publication and state-commit fixtures, to affected regression checks. Retain HT mode-set, VHT/HT capability, response-mode, and MIB unit coverage. C1 checks existing serializer producers and inventories; C4 adds remaining serializer coverage and delayed-policy rejection. Record observed outcomes and their actual failure causes separately from deferred obligations. - -The common fingerprint selection through final C4 covers these simulation paths: - -| Path | Retain after C4: 19 rows | -| --- | --- | -| `showcases/wireless/blockack` | `NoFragmentation`, `MixedTraffic`, `Fragmentation` | -| `showcases/wireless/aggregation` | `NoAggregation`, `Aggregation`, `VoicePriorityAggregation` | -| `showcases/wireless/fragmentation` | `DCFnofrag`, `DCFfrag`, `HCFfrag`, `HCFfragblockack` | -| `showcases/wireless/qos` | `NonQos`, `Qos` | -| `showcases/wireless/txop` | `General`; run 0 | -| `examples/wireless/qos` | `MacQos`, `MacQosWithoutAggregation`, `MacQosWithRtsCts`, `MacQosWithBlockAck`; run 0 | -| `examples/adhoc/qos` | `MacQos`; runs 0 and 1 | - -Block Ack-disabled EDCA rows check the broader lifetime scope. DCF rows provide controls for shared changes. Each row retains its recorded run number, time limit, and fingerprint ingredients; the four wireless QoS rows use run 0. The TXOP row records five seconds, and both ad hoc QoS rows record ten seconds. Block Ack `General` has no separate fingerprint row; direct exchanges and its speed record cover that configuration. - -Each behavior repair must first reproduce its old failure through production code. It compares its corrected result with a pinned parent before another stage changes the same path. S3a records explicit Basic settings without topology or mode changes. Prerequisite tests retain Basic. S5 repeats integration checks with Compressed and rechecks Basic compatibility. - -Tests cover short bursts, sequence wrap, lost responses, queue disposal, uncertain setup, DATA expiration, and reassociation. Each changed fingerprint needs a cause from the stage that introduces it. The implementation must preserve raw results and comparison revisions. S1's four approved changes retain their [separate authority and exact values](../done/ieee80211-compressed-blockack-s1.md). This plan revision authorizes no additional baseline update. - -The [direct verification cases](ieee80211-compressed-blockack-replacement.md#6-verification-and-scope-limits) also cover one peer with two TIDs and buffer sizes below BAR threshold five. They check shifted responses, positive bits for transmitted frames outside the BAR wait, and retry release after a response without overlap. Positive bits must not acknowledge unsent frames or frames outside the active window. HCF module assertions establish these integration results; helper tests and fingerprints cannot replace them. - -Performance checks must separate request contention, response wait, DELBA completion, and total DATA deferral. They must measure delivery, lifetime disposal, teardown disposal, management airtime, and progress for another eligible peer or TID. Teardown measurements must separate inactivity-driven disposal from other causes and record whether discarded DATA still had an unexpired lifetime deadline. Controlled comparisons retain the same traffic, seed, rates, and channel conditions. The plans assume no performance improvement or acceptable loss threshold. - -For bursts with idle gaps, count ADDBA and DELBA exchanges, including retries. Measure application goodput as useful bytes delivered per unit of simulated time over a fixed interval. A separate experimental policy can estimate agreement-reuse effects with otherwise identical inputs. Without that experiment, report observed costs without a claim about agreement-reuse performance. - -S4 needs timing evidence before publication of its changed Basic BAR paths. Test standalone BAR, final BAR after one or four DATA frames, and late continuation near a positive TXOP limit. Exercise single and multiple protection. Compare with S4's pinned parent and apply the exact TXNAV and TXOP conditions. The [umbrella timing contract](ieee80211-compressed-blockack-replacement.md#airtime-acceptance-and-admission-dependencies) owns the required observations and correction boundary. - -Fragmentation suppression needs further airtime checks at C2. At a fixed mode, one unfragmented frame takes longer than one constituent fragment, although the complete fragment sequence can cost more. Block Ack-enabled HCF lacks complete-cost single-protection admission. A positive remaining TXOP interval does not prove that the next complete exchange fits. - -Hypothetical example: a previous DATA exchange ends with 100 microseconds left, but another DATA exchange needs 200 microseconds. The positive remaining interval does not authorize that continuation. C2 must measure the applicable TXNAV and TXOP bounds before C3 or C4 proceeds. Its checks cover Compressed infrastructure fixtures. Retained Basic showcase hops need separate regression observations. - -C3 repeats affected checks across supported response modes and exchange boundaries. No current evidence proves timing compliance for the final configurations. - -Clause 10.23.2.9 permits specific overruns when a TXOP contains at most one Data or Management frame. One permitted case is initial transmission of an unaggregated MSDU under a Block Ack agreement. The MSDU must satisfy the clause's aggregate conditions. The timing record must prove every exception condition and any independent TXNAV requirement. An initial A-MSDU does not qualify for that exception. A second DATA exchange also defeats the one-frame condition. - -Hypothetical example: the first unaggregated MSDU under an agreement needs 200 microseconds within a 100-microsecond TXOP limit. No other Data or Management frame exists in that TXOP, and the aggregate conditions hold. The TXOP exception permits that overrun. The same result for an initial A-MSDU or a second DATA exchange requires a different assessment. - -```mermaid -flowchart TD - M["Measure the complete exchange and TXOP contents"] --> X{"Applicable TXNAV bound satisfied?"} - X -->|"No"| V["Timing violation"] - X -->|"Yes"| L{"Within the TXOP limit?"} - L -->|"Yes"| P["Timing check passes"] - L -->|"No"| E{"All conditions of Clause 10.23.2.9's exception hold?"} - E -->|"Yes"| A["Permitted overrun
No admission-correction prerequisite"] - E -->|"No"| V - V --> N{"Did this stage introduce the violation?"} - N -->|"Yes"| C["Separate correction before affected-path publication"] - N -->|"No"| I["Record the inherited limitation separately"] -``` - -Each responsible stage records permitted overruns, inherited violations, and new violations against its own parent. A permitted overrun creates no admission-correction prerequisite. S4 resolves new unlawful overruns before publication of its affected Basic path. S5 waits for any correction required by Compressed integration and its direct checks. C3 repeats the classification across supported response modes and exchange boundaries. The C2 comparison cannot relabel an unresolved S4 violation as acceptable inherited behavior. - -An alternative configuration restriction needs direct evidence of timing compliance within its stated scope. A fixed MSDU cap alone cannot establish that a later exchange fits the remaining TXOP. The plan selects no automatic size cap or silent Normal Ack fallback. - -The integration source is local `master` at `633b071e213b825ce6019ff3e344ae49f52791ff`, with S1, R1, and S3a merged. Original S1 verification retains feature head `fcc0645035` and publication base `ac6344cc5bcac972012759281a11b3f8255b5d46`. Each remaining stage must pin its actual merged parent and comparison base. Shared HCF paths establish overlap but do not prove a merge conflict. The older lifetime and lifecycle observations require fresh source and caller checks against their selected parents. +## State ownership and failure behavior -S1's final-series record reports debug and release builds, eight recipient scenarios, three NAV runs, and 12 selected fingerprints as successful. Four separately approved fingerprint changes belong to its delivery commit. The [S1 evidence](ieee80211-compressed-blockack-replacement.md#completed-s1-and-recorded-verification) states the exact scope and commands. Those 12 configurations do not verify the proposed 19-row common selection. +The data service previews queued DATA without extraction or sequence assignment. Queue owners perform filtered pull operations when extraction occurs. The originator agreement owns cyclic window operations. The recipient keeps receipt status separate from the reorder buffer that controls delivery. -The earlier 13 October review passed two capability unit fixtures, `Ac_BlockAck`, and the seven-scenario VHT/HT module fixture after a debug build. Those results retain their [historical source and limits](ieee80211-compressed-blockack-replacement.md#branch-verification-record). The plan retains 19 fingerprint rows and ten speed records for later verification. Six speed records cover fragmentation configurations; four cover other Block Ack and TXOP configurations. This document update executes no runtime check. Final performance, lifetime integration, lifecycle repairs, and Compressed behavior remain unverified. +BAR failure updates contention without a DATA retry charge. Failed feedback makes later DATA retransmission set Retry. The finite [DATA lifetime](ieee80211-edca-data-lifetime.md) continues through retries and setup deferral. Final resolution or expiry can trigger completion DELBA even with zero inactivity timeout. -## Scope limits +Ordinary recipient teardown delivers complete buffered units before cleanup. Association reset instead discards old peer state without delivery into the replacement association. Acknowledgment alone does not establish delivery. -The plan excludes these features: +## Compatibility and evidence limits -- A-MPDU and implicit BAR. -- Delayed Block Ack and Multi-TID Block Ack. -- HE bitmap lengths and dynamic fragmentation. -- Groupcast Block Ack. -- New complete-cost TXOP admission and BAR forecasts. +Basic is an INET compatibility profile for the IEEE Std 802.11e-2005 format. It is not Basic support under IEEE Std 802.11-2024. Basic preserves existing fragment and ad hoc behavior. The [implementation status](ieee80211-compressed-blockack-replacement.md) defines configuration and extension contracts. -A-MPDU means several MAC frames within one aggregate. A TXOP is an interval in which a station can start frame exchanges. +Basic single protection reserves only prepared successors; unextracted DATA requires another TXOP. This can increase contention and repeated setup. General Basic DATA admission, complete NAV conformance, Compressed exchanges, A-MPDU, delayed Block Ack, and Multi-TID Block Ack remain outside the implementation claim. -The individual-DATA profile does not establish complete HT-immediate transfer coverage. The plan also makes no stop/start recovery claim. +The [current verification record](ieee80211-compressed-blockack-s4-execution.md) identifies the build, module, unit, protocol, and fingerprint results. It gives exact commands and explicit limits. This summary adds no simulation or performance result. diff --git a/plan/pending/ieee80211-compressed-blockack-replacement.md b/plan/pending/ieee80211-compressed-blockack-replacement.md index a92fbe3d43a..93ae88cae29 100644 --- a/plan/pending/ieee80211-compressed-blockack-replacement.md +++ b/plan/pending/ieee80211-compressed-blockack-replacement.md @@ -1,849 +1,106 @@ -# Implementation plan: add Compressed Block Ack and retain Basic Block Ack +# Basic and Compressed Block Ack implementation status -Integration source: local `master` at `633b071e213b825ce6019ff3e344ae49f52791ff`, with S1, R1, and S3a merged. -Plan revision: 2026-10-15, revision 31. -Review checkout: `/home/user/omnetpp_ws/inet-compressed-blockack-s2`, branch `work/compressed-blockack-s2`, at `d2fcb528b67f9be376caa06d68ea0c1c028ab145`. -Source basis: the pinned integration source and IEEE Std 802.11-2024. Earlier direct PDF inspection supplies the historical IEEE Std 802.11e-2005 evidence. Local S2 source, test, and plan edits are separate from merged master. This review preserves those edits. Remote verification failed because GitHub access was unavailable; the integration pin describes local master. +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc), [HCF parameters](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned) -Approval: the maintainer's 14 October 2026 instruction, "proceed", after the revision-24 review authorizes these corrections to four plan documents. The [S1 source record](../done/ieee80211-compressed-blockack-s1.md) owns its separate implementation, baseline, and publication authorization. This document approval authorizes no later implementation, recorded expectation change, or publication. The [decision table](#maintainer-decisions) records accepted choices and remaining stage requirements. +This checkout implements Basic Block Ack agreement lifecycle and transfer completion. Basic remains the default profile. Production Compressed exchanges are absent. HCF rejects `blockAckVariant = "compressed"` at initialization with `Compressed Block Ack exchanges are not implemented`. -Decision approval: on 14 October 2026, the maintainer accepted the 0.512-second DATA lifetime default, transfer-completion predicate, and individual-DATA scope. The maintainer subsequently selected a 100 ms ADDBA response timeout and a fatal configuration error for active profile conflict. The [decision table](#maintainer-decisions) records all five accepted choices. These instructions authorize their record in the plans, not implementation, baseline changes, or publication. The maintainer's subsequent instructions authorize the review corrections and strict application of the individual-MPDU release clause in revision 30. Its timestamp mapping, event observation, and state ownership are implementation design work, not additional policy decisions. +This document describes the source in this checkout. The [summary](ieee80211-compressed-blockack-replacement-summary.md) gives the main behavior. The [verification record](ieee80211-compressed-blockack-s4-execution.md) gives current results and their limits. -The earlier "proceed" for revision 30 authorizes its text corrections and explicit escalation and publication rules. The current "proceed" authorizes the remaining document corrections for merged R1 and S3a. Strict compliance remains required; no model approximation is approved. +## Previous PRs and S4 scope -S1 merged through [PR #1320](https://github.com/inet-framework/inet/pull/1320) at `1cdb362a2a4b97f564566487079d9d156a5d213d`. Its original verified head is `fcc06450355a52b9c8b0f8f2647e0654711bfaca`. [Appendix A](#completed-s1-and-recorded-verification) preserves that head's scope and recorded evidence. R1 and S3a are also merged, as the [completed prerequisite record](#completed-r1-and-s3a) explains. This revision adds no runtime verification of the combined source. The remaining stages retain their separate review and verification requirements. +The branch `work/compressed-blockack-s4` contains the changes for [PR #1344](https://github.com/inet-framework/inet/pull/1344). Its master base already contains the following merged work. Those mechanisms are prerequisites, not additions from S4. -Revision 31 follows the maintainer's request to account for merged R1 and S3a. It corrects stage status, source assumptions, and dependent verification. It grants no new implementation, baseline, or publication authority. - -This plan defines Compressed support beside retained Basic support and orders its prerequisite changes. The [EDCA lifetime plan](ieee80211-edca-data-lifetime.md) owns DATA expiration. The [agreement lifecycle plan](ieee80211-blockack-agreement-lifecycle.md) owns setup, recovery, and teardown. Section 5 defines separate review and regression boundaries. The complete design is not one implementation change. +| Previous PR | Behavior inherited by S4 | +| --- | --- | +| [#1320: recipient progress](https://github.com/inet-framework/inet/pull/1320) | Receive capacity from agreement values, cyclic delivery, consecutive gap release, and Basic BAR progress before DATA. | +| [#1331: QoS DATA lifetime](https://github.com/inet-framework/inet/pull/1331) | Identity-based deadlines, the 0.512-second default, expiry, aggregation lifetime rules, and safe completion across expiry. | +| [#1343: Basic agreement lifecycle](https://github.com/inet-framework/inet/pull/1343) | Setup validation, Action dispatch, directional teardown, DATA deferral, A-MSDU permission, and recipient teardown disposition. | +| [#1368: association replacement cleanup](https://github.com/inet-framework/inet/pull/1368) | Explicit management-to-MAC peer reset before association replacement, with cleanup before deletion callbacks. | -## 1. Core intended behavior +S4 uses those existing owners to complete outstanding Basic DATA when another DATA candidate cannot proceed. Its production changes have four commit boundaries: -INET will default to immediate Compressed Block Ack for its supported non-DMG HT stations. An explicit Basic profile will preserve the existing legacy demonstrations. An HT station supports high throughput operation. A Block Ack agreement identifies one peer and traffic identifier (TID). ADDBA establishes that agreement. A Block acknowledgment request (BAR) asks the recipient for a Block Ack response. +| S4 commit | Change relative to its parent | +| --- | --- | +| Expose DATA preview without extraction | Adds the QoS preview interface and borrowed candidate access through `InProgressFrames`, with prospective sequence lookup. The concrete data service already supports preview. | +| Mark DATA retries after failed Block Ack feedback | Sets Retry on retransmitted standalone DATA. Existing ACK status already permits that retransmission. | +| Add shared originator window operations | Adds cyclic distance, bitmap membership, and window advancement before their production callers. This preparation changes no packet trajectory. | +| Complete Basic BAR transfer resolution | Selects BAR before extraction, permits BAR-only contention, and separates outstanding groups by peer and TID. It adds matched feedback, control-failure recovery, completion DELBA, and prepared-successor reservation rules. | -The originator will send Compressed BAR only when an immediate agreement applies to the peer and TID. The recipient will send Compressed Block Ack after a valid addressed Compressed BAR for that agreement. The BAR carries a starting sequence number, not a range length. The recipient will build its 64-bit bitmap from its adjusted scoreboard window. Its response starting sequence number may differ from the BAR value. +The final S4 commit documents that integrated result and its verification. The filtered queue API, finite lifetime mechanism, and ordinary recipient teardown delivery remain inherited contracts. S4 connects final resolution or lifetime disposal to window progress and completion DELBA through those contracts. -The originator will apply bitmap outcomes only to frames that the response covers. INET will retain Basic BAR, Basic Block Ack, their C++ classes, serializers, and fragment bitmap support. Basic is a historical compatibility profile, not a variant supported by IEEE Std 802.11-2024. +For example, recipient teardown already knows how to release complete units behind a gap. S4 makes final DATA expiry trigger completion DELBA when no DATA remains. The new trigger uses the existing recipient disposition; S4 does not introduce that disposition. -Hypothetical example: two associated HT stations have an accepted agreement for TID 5 with starting sequence number 4094. The recipient's scoreboard starts at that number. The [lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#3-agreement-behavior) defines setup and DATA eligibility. +Master also supplies profile validation, Basic defaults, and coherent rate-state publication. S4 preserves Basic as the default and retains Compressed initialization rejection. Production Compressed exchanges belong to the separate S5 branch and [PR #1345](https://github.com/inet-framework/inet/pull/1345), outside this checkout. -The originator sends four unfragmented QoS DATA frames with Block Ack policy. No unsent DATA remains. The originator requests channel access for a Compressed BAR with starting sequence number 4094. The recipient sends a Compressed Block Ack after SIFS. Its bits for sequence numbers 4094, 4095, 0, and 1 follow modulo-4096 order. +## 1. Supported behavior -The originator removes acknowledged frames for that peer and TID. Missing frames remain available for retry within the [lifetime contract](ieee80211-edca-data-lifetime.md#3-proposed-ownership-and-behavior). The [lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#first-data-and-final-transfer) owns transfer completion and teardown. +Block Ack support is optional through `isBlockAckSupported`, which defaults to false. With support enabled, HCF coordinates immediate Basic agreements for one peer and traffic identifier (TID). Basic permits the existing non-HT and ad hoc configurations. Its compatibility profile does not require new peer HT capability records. -Under the Compressed profile, absent usable local or peer HT state selects Normal Ack and prevents agreement setup. The Basic profile retains the existing capability assumptions, including ad hoc operation without peer HT records. It does not claim current-standard conformance. The [lifecycle prerequisite](ieee80211-blockack-agreement-lifecycle.md#3-agreement-behavior) owns recovery when a valid frame arrives without an agreement. +The recipient maintains receipt status separately from its reorder buffer. Receipt status controls Block Ack feedback; the reorder buffer controls delivery. A positive acknowledgment therefore does not by itself prove upper delivery. When DATA fills a gap, the recipient releases consecutive complete units in the same operation. A valid Basic BAR can create or advance the reorder record before DATA arrives. -The current `ac` mode supplies HT operation and peer HT state through its inherited HT support. VHT means very high throughput. An associated `ac` peer with usable HT state remains eligible for this profile. The [fixture inventory](#s3a-tracked-fixture-inventory-and-vht-scope) retains `Ac_BlockAck` as a positive requirement. Normal Ack fallback requires an actual capability gap; `opMode = "ac"` alone does not establish one. +The [agreement lifecycle](ieee80211-blockack-agreement-lifecycle.md) owns setup, Action dispatch, directional teardown, and peer reset. The [DATA lifetime contract](ieee80211-edca-data-lifetime.md) owns finite QoS DATA deadlines, with Block Ack enabled or disabled. ## 2. Standard support and model boundary -IEEE Std 802.11-2024 Tables 9-36 and 9-37 assign variant type 0 to Reserved. Both tables assign type 2 to Compressed. Their corpus identifiers are `ieee80211-2024:table:9-36` and `ieee80211-2024:table:9-37`. They appear on physical PDF pages 719–720 and 722–723. - -IEEE Std 802.11e-2005 introduced the Block Ack mechanism and the frame format later called Basic Block Ack. Clauses 7.2.1.7 and 7.2.1.8 define BAR and Block Ack; Clause 9.10 defines their operation. Clause 7.2.1.8 specifies a 128-octet bitmap for 64 MSDUs, with fragment status. Direct inspection of `../inet-standards/80211e-2005.pdf`, relative to the repository root, confirms printed pages 30–32 and 96–100. These are physical PDF pages 52–54 and 118–122. The [IEEE publication record](https://standards.ieee.org/ieee/802.11e/3131/) identifies Amendment 8 and its 11 November 2005 publication. - -Retained Basic support models that historical format and preserves existing INET studies. IEEE Std 802.11-2024 does not support Basic; type 0 remains reserved in that standard. This retention does not claim complete IEEE Std 802.11e-2005 conformance. Existing Basic demonstrations can use later PHY modes and existing INET response behavior. User documentation must distinguish the historical frame format from those model choices. - -Figures 21b and 21e of IEEE Std 802.11e-2005 reserve control bits B0–B11 and assign B12–B15 to TID. INET's retained ACK-policy fields and serializer values are compatibility choices, not field semantics from those figures. C1 preserves the supported Type 0 contract below without a complete historical conformance claim. - -Table 11-8 permits the Compressed profile's immediate agreement when both non-DMG stations are HT stations and ADDBA selects immediate policy. Clause 10.25.2 requires the first DATA sequence number to equal the accepted ADDBA Request starting sequence number. Clause 10.25.6.1 requires HT-immediate Block Ack support for an HT recipient. Clause 9.3.1.7.2 sets the Compressed BAR fragment subfield to zero. Clause 9.3.1.8.2 and Table 9-38 define the Compressed Block Ack bitmap. For a non-HE station, the fragment subfield is zero and the bitmap has 64 positions. - -Clause 10.25.6.5 requires a response after SIFS for an addressed BAR during full-state or partial-state operation. It requires a zero bitmap when no matching partial state exists. This plan selects full-state operation for an established agreement. The [lifecycle standards evidence](ieee80211-blockack-agreement-lifecycle.md#8-standards-evidence-and-scope-limits) covers absent-agreement recovery. - -The [lifetime evidence](ieee80211-edca-data-lifetime.md#2-current-source-and-standard-support) covers the DATA disposal bound. The lifecycle plan separates standard requirements from its transfer-completion interpretation. Clause 9.2.5.6 defines the response Duration/ID subtraction. Clauses 10.6.6.5.2 and 10.6.6.5.4 define primary and alternate response rates. - -This change covers immediate, one-TID Compressed BAR and Compressed Block Ack in the existing non-DMG HT MAC. It uses a 64-position bitmap and an explicit BAR exchange. The Compressed recipient keeps full-state status under Clauses 10.25.6.2 and 10.25.6.3. The selected INET profile sends individual QoS DATA frames before BAR. Clause 10.25.2 describes transfer in A-MPDUs, while Clause 10.25.3 describes BAR for outstanding QoS DATA frames. This plan does not claim complete HT-immediate transfer coverage without A-MPDU. +Basic represents the frame format from IEEE Std 802.11e-2005, Clauses 7.2.1.7, 7.2.1.8, and 9.10. IEEE Std 802.11-2024 Tables 9-36 and 9-37 reserve type 0 and assign type 2 to Compressed. Basic support is an INET compatibility profile. It establishes neither complete IEEE Std 802.11e-2005 conformance nor current-standard Basic support. -### Purpose of the individual-DATA profile +Basic retains its fragment bitmap and existing PHY choices. The shared code contains Compressed frame classes and HT admission predicates. Those components do not establish a production Compressed exchange because HCF rejects that profile before operation. -The accepted user need is controlled study of Compressed bitmap, response, and recovery behavior in INET's current individual-frame MAC. C2 will supply a directly observed Compressed BAR and Block Ack exchange for that purpose. The frame and scoreboard contracts can also support a later A-MPDU design. That later design remains separate. - -The response MPDU decreases from 152 bytes for Basic to 32 bytes for Compressed. This frame-length result does not establish lower exchange airtime or higher application goodput. Control rates, explicit BAR, contention, and repeated setup also affect those results. Section 6 requires direct exchange and airtime evidence without an assumed performance gain. - -On 14 October 2026, the maintainer accepted this intermediate scope for the stated user need. The scope decision before C2 is satisfied. Complete HT-immediate transfer coverage requires a separate A-MPDU design. Wire definitions and independent prerequisite repairs do not establish that complete coverage. - -### Excluded features and fragmentation - -The plan excludes A-MPDU, implicit BAR, HE bitmap lengths, dynamic fragmentation, delayed Block Ack, Multi-TID Block Ack, and GCR. Complete-cost TXOP admission and BAR forecasts also need separate designs. - -Clause 10.2.7 prohibits MSDU fragmentation under the Compressed profile's HT-immediate agreement, even above the fragmentation threshold. Normal Ack does not bypass this restriction. C2 will suppress fragmentation only for that profile's active agreement and peer/TID. - -Basic retains historical fragment handling in the existing fragmentation showcases. Local HT support alone does not turn a Basic compatibility agreement into this Compressed agreement. Outside an agreement, fragmented DATA uses Normal Ack. The lifecycle prerequisite resolves already numbered fragments before setup; C2 tests that boundary with Compressed operation. - -Hypothetical example: the threshold is 250 bytes and a queued MSDU contains 1000 bytes. With an active HT-immediate agreement, the data service extracts it without fragmentation. Without an agreement, the existing fragmentation policy applies and each fragment uses Normal Ack. - -A positive bitmap bit in this profile will acknowledge a complete MSDU or A-MSDU. One fragment alone will not produce a positive bit. Table 9-38 assigns one bit to each unit when the Fragment Number subfield is zero. Clauses 9.4.1.13 and 10.25.6.2 apply the response's A-MSDU permission to DATA for the agreement's peer and TID. A change to Normal Ack alone does not permit A-MSDU DATA when that permission is absent. +The lifecycle document identifies the current INET teardown and recovery choices. General Basic DATA admission and complete NAV conformance remain outside the supported claim. A-MPDU, implicit BAR, delayed Block Ack, Multi-TID Block Ack, and complete VHT transfer behavior are also outside this implementation. ## 3. Current source path and evidence limits -These observations use the pinned integration source, which contains S1, R1, and S3a. The [completed prerequisite record](#completed-r1-and-s3a) separates their evidence from proposed Compressed behavior. The [earlier branch record](#branch-verification-record) reports capability and Basic exchange checks before S1. Historical source observations in the lifetime and lifecycle plans still require review against each selected parent. Each remaining behavior repair must reproduce its old failure through production code before its implementation changes that path. - -[HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) already resolves `blockAckVariant` once into private typed state. Basic remains the default. Unknown values and explicit Compressed selection stop initialization, also when Block Ack support is disabled. S3b/A2 must replace the temporary Compressed rejection before its admission checks can run. C2 supplies the complete exchange path; C4 changes the default. - -Management already publishes local, BSS, and peer rates through R1. Its combined MIB operations commit related HT and rate state before notification. C2 must consume these records; A3b must preserve their callback contract. Identical AP reassociation state can produce no rate notification, so association reset requires a separate handoff for both profiles. - -`Hcf` creates the Block Ack procedures when `isBlockAckSupported` is true. [TxOpFs](../../src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc) chooses the BAR sequence. [BlockAckReqBlockAckFs](../../src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc) builds Basic BAR. The Compressed builder in [OriginatorBlockAckProcedure](../../src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc) throws `Unsupported feature`. - -[Hcf](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) dispatches Basic BAR and Basic Block Ack only. [RecipientBlockAckProcedure](../../src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc) builds only Basic responses. [RecipientQosMacDataService](../../src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc) also accepts only Basic BAR. S1 removed the unusable Compressed request branch from its reorder service. C2 must add Compressed BAR support through the current recipient owners. - -[Ieee80211Frame.msg](../../src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg) defines both variants. Its Compressed Block Ack class has no fixed chunk length. Its common BAR and Block Ack bases still name reserved control bit B0 as an ACK policy. [Ieee80211MacHeaderSerializer](../../src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc) can write and read Compressed fields, but it still treats variant type 0 as Basic. [RecipientQosAckPolicy](../../src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc), [SingleProtectionMechanism](../../src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc), and HCF cost a Basic response length. - -[OriginatorQosAckPolicy](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc) groups outstanding frames by receiver, without TID. [QosAckHandler](../../src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc) can mark other peers' frames as awaiting Block Ack after a BAR. It also has a Compressed bitmap reader. The reader does not, by itself, prove that a received response matches the pending BAR. - -[BlockAckRecord](../../src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc) stores per-fragment reception state. Its getter reports true for an empty map and for absent positions before its earliest retained entry. Neither inference supplies Compressed full-state receipt truth. Each delivery also calls `removeAckStates` through [BlockAckReordering](../../src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc). That call erases earlier entries independently of scoreboard movement. C2 must separate these Basic operations from Compressed receipt retention. - -[ReceiveBuffer](../../src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc) now counts occupied sequence positions from its indexed storage. Fragments of one MSDU count once. Before insertion of newer DATA beyond the window, `BlockAckReordering` advances the window and releases older complete units. It discards incomplete units outside the new window. These S1 operations replace the former fragment capacity limit and out-of-window rejection. - -[InProgressFrames](../../src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc) reports an in-progress frame only when that frame can transmit. [HcfFs](../../src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc) and `TxOpFs` ask for that DATA candidate before they test whether a BAR is due. [Hcf](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) also uses this test before it requests channel access after a frame sequence. Thus, outstanding Block Ack frames alone cannot start a BAR. With the default threshold of five, a finite burst of four frames also lacks a BAR trigger. - -[BasicMsduAggregationPolicy](../../src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc) scans a contiguous prefix from queue index zero. [OriginatorQosMacDataService](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc) calls that policy before it selects one queued frame. It privately owns `QoSSequenceNumberAssignment`. These paths cannot skip a deferred queue head without a change to extraction order. - -The same data service calls `fragmentIfNeeded` after sequence assignment without an agreement check. C2 will use the prerequisite's agreement-aware extraction path to check the selected peer and TID before that call. HCF supplies the current agreement state; the data service keeps no duplicate agreement store. This check prevents fragmentation before frame construction, rather than a later ACK-policy change. - -[Ieee80211Mib](../../src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h) already owns local HT capability and peer HT state. Management updates that state on association changes. The Block Ack policies do not use it. The current [802.11n protocol test](../../tests/protocol/wifi/11n/N_CompressedBa.test) has a Compressed name, but it expects a Basic Block Ack. [Ieee80211RecipientNav_1.test](../../tests/module/Ieee80211RecipientNav_1.test) checks response NAV behavior with Basic Block Ack. [Ieee80211HcfMultipleProtection_2.test](../../tests/module/Ieee80211HcfMultipleProtection_2.test) checks response duration with synthetic Basic frames. - -The [agreement lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#2-current-source-and-required-evidence) owns the setup, Action, inactivity, and teardown findings. The [lifetime plan](ieee80211-edca-data-lifetime.md#2-current-source-and-standard-support) owns missing lifetime integration and sequence-key collisions. - -The Block Ack-enabled single-protection path in HCF calls `InProgressFrames::getPendingFrameFor` even for BAR. That operation can extract and number DATA. BAR duration needs no successor DATA. - -[BlockAckReordering](../../src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h) now returns complete units through `ReleasedFrames`, an ordered vector of sequence-number and fragment-group pairs. `RecipientQosMacDataService` preserves that order through reassembly, A-MSDU deaggregation, and delivery. A release across sequence wrap therefore delivers 4095 before 0. - -The [DATA reception path](../../src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc) now releases every consecutive complete unit after DATA fills a gap. The reorder owner creates its record on the first valid related DATA or Basic BAR from the accepted agreement. A Basic BAR before DATA can therefore advance an empty window. The S1 fixture supplies accepted agreements; it does not prove negotiated values or a Compressed exchange. - -The [response-rate method](../../src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc) accepts only Basic BAR. The originator BAR timeout and recipient response duration both call it. C2 therefore needs Compressed response-rate and duration support before its first production exchange or airtime assessment. C3 cannot supply that prerequisite after C2's required evidence. - -The [IEEE 802.11 fillers](../../tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc) already contain Basic and Compressed BAR and Block Ack producers. The Compressed producers set old ACK-policy fields and nonzero reserved values for field-preserving round trips. C1 must adapt those producers to its reserved-bit contract with the serializer change. [Ieee80211ResponseModeSelection_1.test](../../tests/unit/Ieee80211ResponseModeSelection_1.test) constructs Basic BAR and uses a 152-byte response length. It requires an HT Basic response mode, which Basic retention preserves. Compressed needs additional 32-byte non-HT response cases. - -## 4. Proposed design - -### 4.1 Select the current or historical profile - -HCF already resolves `blockAckVariant` once into a private typed profile value during initialization. The values remain `"compressed"` and `"basic"`; Basic is the current default and Compressed is the final default. Preserve rejection of unknown values. A2 replaces the temporary Compressed rejection; C4 changes the default after runtime verification. The resolved value remains immutable throughout each agreement's lifetime. Both endpoints must select the same profile; this plan supports no runtime profile change. - -HCF owns local profile selection. Merged S3a keeps its typed profile private and exposes no profile query. A2 must pass that value to agreement setup through the smallest interface its actual consumers require. C2 must pass it to frame exchanges and their dependent procedures. Policies, builders, records, protection, and timeouts must not independently read the parameter or infer the profile from PHY mode. The frame context supplies the expected BAR and response variant without a variant registry or second agreement store. - -The serializer independently decodes the actual wire type. It must not consult HCF configuration. Format-specific bitmap and length operations remain necessary, but they consume the decoded frame or resolved exchange context. `isBlockAckSupported` still controls procedure construction. ADDBA gains no variant negotiation field. - -Keep the common BAR and Block Ack bases, both derived variants, and the Basic length constant. Retain existing Basic builders and duration methods for their callers. Add Compressed operations where existing interfaces cannot serve both variants. HCF will supply its resolved profile to the existing policies and procedures. New callers must use the actual request variant for response length and rate selection. - -Type 0 will produce Basic typed frames for historical compatibility. Type 2 will produce Compressed typed frames. Other unsupported types will follow the invalid-frame path. - -- Decode the complete four-bit BAR Type and BA Type fields. -- For Compressed, write zero in reserved control bits. -- For Compressed, ignore received reserved-bit values. -- Preserve the common C++ fields and Basic values that encode Type 0. - -The current nine-bit `reserved` field maps to B3–B11. Its two low bits therefore overlap the expanded Type field at B3 and B4. For Basic serialization, those two bits must be zero. Preserve the other Basic field values and existing round trips within that Type 0 contract. Reject a Basic object that would encode another Type before serialization. The [BAR writer](../../src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc) and Block Ack writer both encode `(getReserved() & 0x1FF) << 3`. C1 must verify this layout with direct octet assertions for both frame types. Do not guess a historical layout from frame length or local profile configuration. - -Hypothetical example: a Basic object has `reserved = 1`. The old writer places that value in B3, which a complete four-bit decoder reads as Type 4. C1 rejects that inconsistent Basic object before serialization. - -For this Compressed profile, B0 and B5–B11 are reserved. B3 and B4 remain Type bits, so they cannot use the reserved-bit ignore rule. Compressed ACK-policy and `reserved` model fields have no field-preserving wire contract. C1 sets those fields to zero in the existing Compressed round-trip producers. Separate octet checks verify zero writes and ignored received reserved bits. C1 updates both fixture inventories with its serializer change; C4 adds any remaining coverage. - -The existing Compressed branches in `QosAckHandler` do not establish a production Compressed exchange. - -The MSG generator will regenerate derived C++ files. Do not edit generated files by hand. - -Set the Compressed Block Ack chunk length to 28 bytes. Its fields contain 16 MAC-header bytes, 2 control bytes, 2 starting-sequence bytes, and 8 bitmap bytes. `Ieee80211MacTrailer` supplies the 4-byte FCS, so the transmitted MPDU is 32 bytes. Keep one source for the chunk length. Use the selected PHY mode to calculate airtime. Do not add a second duration cache. - -The Basic response remains 152 bytes with FCS; Compressed remains 32 bytes with FCS. Protection and timeouts must use the request's actual response variant. The Compressed rate rules below must not replace the retained Basic response contract. A received response must match the pending BAR variant before it can report success. - -A profile mismatch cannot silently select Basic or acknowledge DATA through the other variant. ADDBA cannot detect this configuration mismatch. The [lifecycle mismatch contract](ieee80211-blockack-agreement-lifecycle.md#profile-conflict-detection-and-termination) requires HCF to stop the simulation on a valid conflicting BAR for an active agreement. Corrected endpoint profiles require a new run. - -The prerequisite plans own eligibility queries, sequence preview, Action outcomes, lifetime operations, and duplicate-state cleanup. The recipient stage below owns ordered delivery. These changes extend beyond the Basic-specific contracts; each stage requires its own caller and compatibility checks. - -### Migration diagnostics - -After local capability initialization, HCF will log the resolved profile for each interface with Block Ack enabled. If Compressed has no usable local HT support, HCF will emit one initialization warning for that interface. The warning will state the Normal Ack result and the explicit Basic compatibility setting. Basic selection and disabled Block Ack must not produce that warning. - -The local warning cannot detect an HT ad hoc station without peer HT records. The agreement policy's existing decision log must identify a refusal caused by absent local or peer HT state. Use that log to expose the fallback reason without warnings for every DATA frame. Normal association startup and temporary peer-state loss must retain their existing protocol behavior. - -C4's migration fixture will use a user-style configuration outside the legacy override inventory. Supply a queued successor so the production setup policy actually checks eligibility. It will enable Block Ack and omit `blockAckVariant`. Test legacy `g` and `a` modes, plus HT ad hoc management without peer HT records. Require Normal Ack DATA and no ADDBA, BAR, or Block Ack. Check the resolved-profile message, applicable local warning, and policy refusal reason. - -Repeat with explicit Basic at both endpoints and require successful Basic exchanges. Repeat with Block Ack disabled and require no irrelevant warning. Documentation must explain the local warning's limit and the HT ad hoc case. The mode set must not change the selected default. - -### 4.2 Consume the agreement prerequisite - -The [agreement lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#3-agreement-behavior) supplies DATA eligibility and accepted parameters for each peer and TID. HCF must obtain that state before it selects Compressed Block Ack policy. The Compressed builder consumes the current agreement; it cannot restore an agreement that its owner removed. - -Hypothetical example: the lifecycle owner retires the agreement before DATA 101 becomes eligible. The Compressed builder must not use the old agreement to transmit DATA 101. - -The Compressed addition retains the prerequisite's DATA eligibility, Action ownership, and recovery results. It does not add a second agreement store or reproduce the lifecycle state machine. - -HCF will validate a Compressed BAR before it calls `RecipientQosMacDataService::controlFrameReceived`. This profile requires Fragment Number zero and the supported frame length. An invalid BAR will reach neither the reorder service nor the response procedure. Unknown variants and malformed frames follow the invalid-frame path without protocol recovery actions. A well-formed retained BAR for the other profile is a separate conflict case. For a matching active peer/TID, HCF raises the configuration error before scoreboard, reorder, response, or inactivity updates. - -After format validation, HCF will use the absent-agreement path from lifecycle stage A3c. C2 will verify that valid Compressed BAR reaches that path and malformed BAR does not. The lifecycle plan owns DATA disposal, Action ownership, and the resulting teardown state. - -### 4.3 Keep BAR selection and response state exact - -`OriginatorBlockAckAgreement` will own `WinStartO` and `WinSizeO` under Clause 10.25.6.2. A positive accepted ADDBA Response buffer size will set `WinSizeO` to the smaller of that size and 64. The ADDBA Request starting sequence number will initialize `WinStartO`. The first DATA frame under the accepted agreement must use that request sequence number under Clause 10.25.2. - -The [agreement lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#first-data-and-final-transfer) owns expiration before the first DATA transmission and its required teardown. - -The existing originator ACK policy will group outstanding Block Ack frames by peer and TID. It will choose the oldest frame that needs an initial BAR within that agreement's transmit window. The comparison will use modulo-4096 arithmetic. The BAR will carry that starting sequence number and no range length. - -The policy will apply the threshold separately to each peer and TID. It may defer BAR below that threshold only when the next selected DATA belongs to that agreement and fits its window. A candidate for another agreement or management work will not defer that group's BAR. With no DATA candidate, any nonempty eligible group can request BAR below threshold. Before outside-window DATA, the policy will request BAR for older frames with `BLOCK_ACK_NOT_YET_REQUESTED` status. It will transmit older retry-eligible frames before that outside-window candidate. - -If several groups need BAR, the policy will select the group whose oldest outstanding frame entered the in-progress queue first. Peer address and TID will resolve equal-time ties. The policy will derive this order from existing frames without a separate scheduling queue. Hypothetical example: A has four outstanding frames while B supplies continuous DATA in the same access category. Before the next DATA for B, the policy selects BAR for A despite the threshold of five. - -HCF will preview the next eligible candidate before it selects a frame sequence. For a DATA candidate, the originator ACK policy will compare its peer, TID, and sequence number with its agreement window. `HcfFs` and `TxOpFs` will select BAR without extracting DATA. They will also handle a null candidate when only BAR work remains. After older positions resolve, `TxOpFs` will extract and use the DATA candidate. - -`OriginatorBlockAckAgreement` will advance `WinStartO` past contiguous acknowledged or dropped positions before HCF removes their ACK records. This update includes resolved Normal Ack DATA within the agreement's sequence space. When no unresolved frame remains, it may move `WinStartO` to the next unsent sequence number. Clause 10.25.6.7 permits transmission beyond the old window with that advance. This reuses the existing pending queue and adds no duplicate window state in `QosAckHandler`. A BAR after the final short burst needs no new timer. - -After the final exchange, HCF will apply the [lifecycle plan's transfer-completion rule](ieee80211-blockack-agreement-lifecycle.md#first-data-and-final-transfer). The channel-access stage must make that final BAR possible before it enables completion-triggered DELBA. - -HCF will treat an eligible BAR as channel-access work for the correct access category. It will request contention after a frame sequence when only such work remains. The frame sequence will select a BAR with no DATA pointer. The internal-collision path will keep BAR work pending without giving DATA retry feedback. The existing EDCA, TXOP, and protection decisions still control when that BAR can transmit. - -When HCF transmits BAR, `QosAckHandler` will mark only transmitted frames for that BAR's peer and TID as `WAITING_FOR_BLOCK_ACK`. Those frames must have `BLOCK_ACK_NOT_YET_REQUESTED` status and lie within the active agreement window at or above the BAR number. It will leave every other peer and TID unchanged. It will also leave frames outside that window unchanged. Its current per-frame status map will identify the BAR's waiting frames; it needs no second set. - -The frame sequence will build Compressed BAR for the selected peer and TID. Its receive step will match the response type, receiver, transmitter, and TID against the active BAR. Only a matched, format-valid response can reach the success callbacks for recovery, agreement, and ACK-state handlers. The response starting sequence number may differ from the BAR value under Clause 10.25.6.5. HCF will pass the active agreement window to `QosAckHandler` for this response; the handler will keep no window copy. - -Before that success path, the receive step will also require Fragment Number zero, exactly 64 bitmap bits, and the supported frame length. The header chunk is 28 bytes; the MPDU includes the 4-byte FCS. These checks apply to deserialized input and directly supplied typed frames. An unsupported or malformed response cannot update the bitmap, refresh inactivity, or report exchange success. It ends the receive step through the existing failed-BAR path, which releases requested DATA for retry. Hypothetical example: type 2 with a dynamic-fragmentation encoding fails even when its peer and TID match. - -If teardown or peer cleanup ends that agreement during the BAR exchange, HCF will retain the exchange context until completion. A later response or timeout will not access removed agreement state or recreate it. HCF will resolve the current agreement through its owner before it applies bitmap outcomes. This guard covers an actual event boundary and requires no duplicate window state. - -Under Clause 10.25.6.8, `QosAckHandler` will accept positive bits for unacknowledged transmitted frames in that window. This includes covered frames outside the BAR's starting position and frames that did not wait for this BAR. For zero bits, it will keep covered frames unacknowledged. It will make the BAR's covered waiting frames retry eligible. It will not change the acknowledgment state of frames below the response starting sequence number. - -It will clear their wait for this BAR so later retry selection can proceed. A response with no overlap will leave all requested frames unacknowledged and eligible for DATA retransmission. A timeout will do the same. This profile selects DATA retransmission rather than repeated BAR-only polling after a failed exchange. The originator agreement window will advance from resolved frame status, not from the response starting sequence alone. - -Hypothetical example: the transmit window covers sequence numbers 100 through 163, and BAR starts at 104. A valid matched response starts at 100 and has a positive bit for transmitted DATA 102. The handler acknowledges DATA 102 because the received bitmap and transmit window cover it. The BAR's starting number does not exclude that acknowledgment. - -### 4.4 Consume the DATA lifetime prerequisite - -The [EDCA lifetime plan](ieee80211-edca-data-lifetime.md#3-proposed-ownership-and-behavior) supplies the existing DATA deadline to the Compressed consumer. A bitmap result or BAR timeout must not restart that deadline. The lifetime plan owns identity, expiration, and safe completion. - -Individual Block Ack DATA retransmission will preserve its sequence number and set Retry to one under Clause 9.2.4.1.6. Its exception does not include non-A-MPDU frames. Clause 10.23.2.12.1 alone does not establish an optional Retry bit. Zero bitmap bits and BAR timeout will not become synthetic failures in the Normal Ack retry counters. Existing EDCA recovery still owns channel-access effects; the new lifetime integration supplies the missing disposal limit. Internal collisions retain applicable recovery accounting, and no DATA counter will identify frames by TID and sequence alone across different peers. - -Hypothetical example: every Block Ack reports DATA 100 as absent but acknowledges later DATA. Successful responses refresh agreement inactivity, but they cannot extend DATA 100's lifetime. HCF eventually discards 100 and permits window progress. - -The Compressed replacement cannot omit this disposal bound while it retains the proposed retransmission behavior. - -### 4.5 Build recipient state and select response airtime - -`BlockAckRecord` will own the full-state scoreboard window for each accepted agreement. At establishment, it will set `WinStartR` from the ADDBA request. It will set `WinSizeR` to the accepted response buffer size, which this profile caps at 64. It will derive `WinEndR` from those values. - -The existing reorder buffer will use that same accepted size. For each valid related DATA frame, the scoreboard will apply Clause 10.25.6.3 before reorder insertion or duplicate rejection. This update applies regardless of that DATA frame's ACK policy. - -Retain the Basic record's fragment status. The Compressed response will derive one complete-unit receipt bit per sequence position from the same agreement-owned record. Unfragmented DATA supplies a complete MSDU or A-MSDU in that profile. A partial Basic unit cannot become a positive Compressed bit. A received Compressed BAR will advance the scoreboard under Clause 10.25.6.3, with modulo-4096 comparisons. - -Compressed receipt status remains after delivery. Scoreboard movement controls its retirement; reorder delivery must not call the Basic deletion operation for that profile. Preserve the receipt evidence at `WinStartR - 1` for the individual-MPDU delivery condition below, even outside the response window. That evidence cannot set response bits outside `WinStartR` through `WinEndR`. An empty record has no positive bit. Retain the Basic fragment representation and its existing operations. - -At C2, HCF will reject fragmented DATA for an active Compressed agreement before scoreboard or reorder processing. Nonzero Fragment Number or More Fragments set identifies that input. HCF will use the invalid-frame path without agreement recovery actions. Such input cannot allocate fragment receipt state, produce a positive bitmap bit, or cause DATA delivery. Basic agreements retain fragment receipt state and reassembly after C2 and C4. Ordinary Normal Ack reassembly outside an agreement retains its existing owner. - -The response will use the adjusted `WinStartR` as its starting sequence number. Bits beyond `WinEndR` will be zero. - -`BlockAckReordering` owns delivery order and `WinStartB`. S1 makes `WinSizeB` count MSDU or A-MSDU sequence positions. Basic `ReceiveBuffer` retains multiple fragments at one position, which counts once against the supplied agreement size. A fragment that completes that unit remains eligible when its position occupies the last available window slot. For newer DATA beyond `WinEndB`, the reorder owner moves the window, retains the new DATA, and releases eligible older complete units. C2 will reuse these operations for the Compressed profile under Clause 10.25.6.6 and retain Basic fragment behavior. - -S1 releases every consecutive complete unit from `WinStartB` when DATA fills a gap. It stops at the next absent or incomplete unit. The reorder owner advances the window after the last released unit without a later DATA reception or BAR. Its selected rule follows Clause 10.25.6.6.2.1(a)(2), with Basic retention as an INET compatibility choice. The S1 fixture buffers complete DATA 101 and 102 behind absent DATA 100. Receipt of DATA 100 releases 100, 101, and 102 in that order in the same operation. - -C2 must implement Clause 10.25.6.6.2.1(a)(2)(v) with its surrounding conditions for the supported individual-MPDU path. The received DATA must belong to an HT-immediate agreement without segmentation and reassembly. The station must be non-EDMG. - -RIFS means reduced interframe space. The sequence number must lie inside the reorder window and exceed `WinStartB`. The receipt bit at `WinStartR - 1` must be positive. The MPDU must follow the preceding MPDU by SIFS or RIFS, or follow ACK transmission by SIFS. - -When these conditions hold, the recipient must apply the clause's sequential delivery and window-update rules despite the gap. If either receipt evidence or the interframe relation is absent, this condition does not permit release. Another applicable release rule can still permit delivery. C2 must not omit this behavior or make it optional merely to avoid event history. - -The reorder owner releases eligible units in cyclic order under that clause. It continues sequentially until the next sequence position has no buffered unit. It advances `WinStartB` after the last released unit. The A-MPDU exclusion does not exclude this individual-MPDU condition. The originator retains its current SIFS frame sequences. - -HCF supplies the actual interframe relation to the reorder operation. The [receiver](../../src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc) supplies reception start and end times through `SignalTimeInd`. HCF receives ACK completion through its current transmission callback. The [mode interface](../../src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h) supplies the applicable SIFS and RIFS intervals. Those receive and completion paths retain no previous-MPDU or ACK history for this decision. C2 retains only that required event history in HCF. - -### Interframe history and timing design - -The individual-MPDU release rule adds behavior to Compressed reception. It can change delivery times and fingerprints when its conditions hold. Basic retains S1's delivery rules. A changed Basic fingerprint requires its own causal evidence; the Compressed release rule does not justify that change. - -As part of C2, its owner must derive the history and timing contract against the selected source parent. The existing callbacks identify possible inputs, but they do not establish a complete contract. Record the implementation design and its source and standards evidence: - -- Derive the qualifying previous reception or ACK transmission from the complete standard procedure. Do not impose extra same-peer, address, or TID restrictions without a standards basis. -- Specify the minimal stored timestamps and validity fields. Retain values rather than frame pointers beyond their ownership lifetime. -- Define which intervening receptions and transmissions replace or invalidate history, including unrelated traffic and failed reception. -- Clear relevant validity on agreement retirement, peer cleanup, and replacement setup. Establish invalid history at initialization; add no stop/start support claim. -- Define when HCF evaluates the current frame against previous history and when it replaces that history. Preserve the evidence needed by reorder processing. - -A per-peer last-frame timestamp alone cannot establish the immediately preceding event. An intervening frame from another peer can break the relation. The production trace must establish how HCF observes such events before the owner selects its storage layout. Do not add a history registry or new timer without a demonstrated need. - -Define the receive-side interval from previous reception end to current reception start. Define the ACK-side interval from local ACK transmission end to current reception start. `SignalTimeInd` supplies receive endpoints; the transmission callback supplies local completion. Current-frame delivery time is not its reception start. - -For a stationary sender, equal propagation delays can cancel between successive receptions. After local ACK transmission, the next DATA reception can include outward and return propagation delay. Therefore, raw equality to SIFS is not a complete rule for both paths. Map the applicable PHY timing definitions to INET timestamps, including signal extension and propagation effects. Record time resolution, supported coverage assumptions, and the applicable RIFS operation conditions. - -An available RIFS duration alone does not establish permission to transmit RIFS-separated frames. - -Clauses 10.3.2.3.2 and 10.3.2.3.3 define RIFS and SIFS timing; Clause 10.22.5 defines coverage-class behavior. These clauses constrain the implementation; they do not make timing tolerance a maintainer preference. A proposed receiver-side recognition interval must have a documented derivation from those rules and the model. No inferred interval or arbitrary epsilon is accepted by this plan update. - -The exact timestamp mapping, event observation coverage, and stored fields still need source analysis and design. The C2 owner must resolve and review them before the affected implementation is complete. These are engineering obligations within the required behavior, not new policy decisions or grounds to omit the clause. Independent R1 work remains unaffected. Tests must cover nonzero propagation delay, intervals outside the permitted relation, and intervening unrelated frames. - -If existing PHY observations cannot establish the required timing relation, the C2 owner must record the specific gap and a concrete example. The owner must propose the smallest correction to the observations or their interpretation. The code reviewer must check that proposal against the standard and the supported PHY model. Missing callback data alone does not establish that the required behavior cannot be modeled. - -If the correction requires a model approximation or reduced scope, the maintainer must approve an explicit plan revision before its implementation. The revision must state the changed behavior, evidence, and compliance limits. Approval does not make an approximation equivalent to strict compliance. Until resolution, the affected C2 path remains incomplete and cannot be published. Independent work that neither exposes nor depends on that path may continue. - -Local experimental code may support the design investigation. The affected Compressed release path must not enter a published implementation until its design review and direct verification pass. This publication boundary requires no runtime switch to disable an unfinished rule. These provisions add no implementation or publication authorization to this document-only update. - -HCF passes the interframe result with the frame. It adds no timer, scheduler, or simulation event for this relation. The agreement-owned scoreboard supplies the bit at `WinStartR - 1` before window cleanup can erase it. The reorder owner consumes that bit with the interframe result. A false receipt bit or absent interframe relation leaves an in-window gap buffered under the ordinary rule. The two conditions remain independent. - -Hypothetical example: an accepted agreement starts at 100 with buffer size four. The recipient receives and delivers Normal Ack DATA 100, then sends ACK. Its reorder start becomes 101. In this zero-propagation example, DATA 104 reception starts one SIFS after local ACK transmission ends. The scoreboard start becomes 101. Receipt evidence for 100 satisfies condition (v), so the recipient delivers 104 despite absent positions 101–103. Without that evidence or SIFS relation, it retains 104 until another applicable release condition occurs. - -S1 returns released units as an ordered sequence of fragment groups through `ReleasedFrames`. Its traversal uses modulo-4096 order relative to the old reorder window. `RecipientQosMacDataService` preserves that order through defragmentation, A-MSDU deaggregation, and delivery. The receive buffer retains its indexed storage. External callers must consume the supplied order, as the [migration guide](../../doc/src/migration-guide/index.rst) requires. C2 will preserve this contract for Compressed reception, with constituent MSDUs in order within each A-MSDU. - -S1 discards incomplete units outside the new window. Its Basic BAR path applies the selected reorder rule from Clause 10.25.6.6.3.1, also before DATA reception. `BlockAckReordering` initializes the record from the accepted agreement before it applies that frame's window update. A valid Basic BAR can advance an empty reorder window, whose new start remains available for later DATA. C2 will add validated Compressed BAR input to this current path. - -Hypothetical example: the accepted request starts at 100 with buffer size eight, but the recipient receives no DATA. A valid BAR at 104 creates the reorder record and moves its start to 104. Subsequent DATA 104 is eligible for immediate delivery. Invalid BAR and BAR without an agreement create no reorder record. The reorder window will not depend on the scoreboard's `WinStartR`; the two owners track different state. Do not add another scoreboard store or a duplicate reorder buffer. - -This profile will send the Compressed Block Ack response in a non-HT PPDU. `QosRateSelection` will use the BAR rate for a non-HT BAR. It will use the non-HT reference rate for an HT or VHT BAR. It will select the highest qualifying BSS basic rate at or below that value. If none qualifies, it will select the highest qualifying mandatory PHY rate. The MIB supplies rate-set storage, and `IIeee80211Mode` supplies the reference rate. - -For a non-HT BAR, the response will use the BAR's modulation class and preamble type. For an HT or VHT BAR, it will use the OFDM or ERP-OFDM modulation class. Rate candidates must satisfy that modulation rule. A configured response-rate override must select the primary rate or a legal alternate under Clause 10.6.6.5.4. An alternate must use the same modulation class, satisfy the basic or mandatory rate rule, and preserve response duration. `QosRateSelection` will reject an invalid override with a clear configuration error before response transmission. - -Originated Compressed BAR will use a non-HT PPDU as a fixed INET profile choice. This choice permits legal BAR transmission without new VHT capability negotiation or protected HT/VHT mode selection. `QosRateSelection` will select the highest eligible local legacy mode under the rules below. Other control frames retain their existing mode contract. A valid received HT or VHT BAR remains supported through the response rules above. - -- When BAR initiates a TXOP, Clause 10.6.6.2 requires a BSS basic rate. If that set is empty, the selected rate must be mandatory for the attached PHY. -- For a later BAR, Clause 10.6.6.4 requires a rate from the peer's known operational rate set. If that set is unknown, use a BSS basic rate, or a mandatory rate when the basic-rate set is empty. -- Mode candidates must belong to the local legacy operational set. When BSS rate state is unknown, mandatory-rate selection is the documented INET fallback. A configured `controlFrameBitrate` must satisfy the same BAR conditions; otherwise `QosRateSelection` rejects that BAR before transmission. - -At the pinned revision, [MIB rate sets](../../src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h) supply storage and accessors, but no production caller populates them. The [rate-set type](../../src/inet/linklayer/ieee80211/mib/Ieee80211RateSet.h) defaults to unknown state. [Management](../../src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc) constructs Supported Rates elements without publication to the MIB. Ordinary association therefore does not supply the rate knowledge required here. Direct MIB injection verifies consumer logic, not production publication. - -The separate [R1 stage](#r1-publish-production-rate-state) will populate the existing MIB records from advertised or accepted rate information. R1 owns full and simplified management, Beacon refresh, cleanup, and publication tests. C2 consumes the completed rate contract before its first exchange. It will repeat the subscriber interaction after S3b installs HCF's lifecycle callback. - -Rate-only publication must preserve unchanged agreements in either profile. Missing HT state must not retire Basic compatibility. R1 creates no new rate store, parameter, or negotiation mechanism. - -The current [TXOP initiator predicate](../../src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc) always returns false. [QosRateSelection](../../src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc) uses that predicate to distinguish initial and later control frames. C2 cannot reuse that result for Compressed BAR classification. [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) already has the frame-sequence context before mode selection. HCF will derive whether this BAR starts the TXOP from actual frame-sequence progress. `QosRateSelection` will receive that BAR-specific condition through the mode-selection call path. - -A BAR after fresh channel access will use the initial-BAR rule. A BAR after an earlier transmission in the same TXOP will use the later-BAR rule. This change preserves other control-frame decisions and adds no retained classification flag. The mode catalog supplies eligible local modes and mandatory-rate status. - -Hypothetical example: BSS basic rates are 6, 12, and 24 Mbps, while the peer also supports 54 Mbps. The initial Compressed BAR uses the eligible 24 Mbps non-HT mode. A later BAR can use the eligible 54 Mbps mode. Production checks must establish both rate publication and BAR position before they compare these selected modes. - -When no basic rate qualifies, Clause 10.6.6.5.2 supplies the applicable mandatory-rate rule. If the BSS basic-rate set is unknown, mandatory-rate selection is an explicit INET fallback. The response Duration/ID will use Clause 9.2.5.6 and actual Compressed airtime. The single and multiple protection paths will use the new frame length without new TXOP admission policy. - -R1 supplies committed production rates. C2 supplies BAR classification, response-rate rules, override checks, and Compressed airtime calculations before its first exchange. Its BAR timeout, recipient Duration/ID, and protection paths will all use the supported Compressed response mode and MPDU length. C2 will record the selected mode with its first timing result. C3 will add focused verification across response modes and exchange boundaries; it supplies no missing runtime operation required by C2. - -For BAR, HCF will bypass `getPendingFrameFor` and use the BAR duration operation in `SingleProtectionMechanism`. It needs only the selected BAR, SIFS, and that variant's actual response airtime. It will receive no successor DATA and perform no extraction. Other duration paths retain their existing scope. Hypothetical example: peer A needs BAR while peer B has queued DATA. Both BAR selection and its duration calculation leave B's queue ownership and sequence counter unchanged. - -## 5. Separate changes and dependencies - -### Maintainer decisions - -The maintainer accepted all five decisions on 14 October 2026. Their decision conditions are satisfied; source reviews, stage prerequisites, and verification remain required. S1 recipient repair and R1 rate publication retain independent review and publication boundaries. - -| Decision | Status and recorded result | Dependent boundary | -| --- | --- | --- | -| Lifetime compatibility | The maintainer accepted the 0.512-second default on 14 October 2026. It applies to QoS DATA with Block Ack enabled or disabled. The [lifetime record](ieee80211-edca-data-lifetime.md#lifetime-compatibility-decision) states the compatibility effect and required measurements. | The decision condition for S2/L1 is satisfied. | -| Response timeout | The maintainer accepted a positive, finite 100 ms default on 14 October 2026. The [lifecycle contract](ieee80211-blockack-agreement-lifecycle.md#response-timeout-and-data-lifetime) defines its start, recovery, and DATA-lifetime interaction. | The decision condition for A4 is satisfied. | -| Transfer completion | The maintainer accepted the [queue predicate](ieee80211-blockack-agreement-lifecycle.md#transfer-completion-decision) on 14 October 2026. At an exchange boundary, the agreement has no queued DATA, in-progress DATA, or pending BAR exchange. Conditional DELBA remains required. | The decision condition for A5 in S4 is satisfied. | -| Individual-DATA purpose | The maintainer accepted the [intermediate profile](#purpose-of-the-individual-data-profile) on 14 October 2026. Its purpose is bitmap, response, and recovery studies with individual DATA before explicit BAR. Complete HT-immediate transfer remains outside that scope. | The scope decision before C2 is satisfied. | -| Persistent profile conflict | The maintainer accepted a fatal configuration error on 14 October 2026. The [conflict contract](ieee80211-blockack-agreement-lifecycle.md#profile-conflict-detection-and-termination) requires termination before state updates and a new run with corrected profiles. | The decision condition for C2 mismatch implementation is satisfied. | - -### Stage conditions - -The table separates conditions for stage start, continuation, completion, and publication. Each condition blocks only its dependent work. Review of this umbrella plan does not establish either prerequisite plan's correctness or completed verification. - -| Boundary | Responsible owner and required condition | -| --- | --- | -| Before each stage starts. | The stage owner pins the source parent and comparison base under the source-base rules below. | -| Before S2 implementation. | The lifetime reviewer completes the separate [lifetime review](ieee80211-edca-data-lifetime.md). The accepted default is 0.512 seconds; its compatibility decision is satisfied. Before the production change, the implementation owner reproduces absent queue expiration through HCF. | -| Before S2 completes. | The lifetime owner supplies [measured expiry under controlled congestion](ieee80211-edca-data-lifetime.md#5-verification-and-evidence), including a Block Ack-disabled case. A selected regression row without expiry cannot satisfy this condition. | -| S3a preparation. | S3a is merged. Preserve its recorded parent comparison and explicit Basic consumers. Repeat affected checks after shared changes. | -| Before S3b implementation, including A1. | The lifecycle reviewer completes the separate [lifecycle review](ieee80211-blockack-agreement-lifecycle.md). Select a parent that contains merged S3a and R1. Refresh selector, MIB commit, reset, and callback paths against that parent. | -| Before A3b implementation. | Refresh the [consumer assessment](ieee80211-blockack-agreement-lifecycle.md#reassociation-consumers-and-callback-order). Trace Compressed HT removal and replacement. Trace Basic association-reset notifications with the actual peer key. | -| Before A4 implementation. | The lifecycle review checks the [accepted 100 ms timeout](ieee80211-blockack-agreement-lifecycle.md#response-timeout-and-data-lifetime) against the selected source parent. The implementation owner completes S2, A1, A2, and A3a through A3c. | -| Before S4 starts. | The S4 owner confirms that S3b supplies the required teardown behavior and S1 supplies recipient progress. A5 uses the accepted transfer-completion predicate; its decision condition is satisfied. | -| Before S4 completes or its affected Basic path is published. | The S4 owner compares changed BAR timing with S4's pinned parent. Resolve any new unlawful overrun under [Section 6](#airtime-acceptance-and-admission-dependencies) before that publication. This condition requires no future C2 or C3 result. | -| Before S5 starts. | The implementation owners complete the prerequisite contracts from S1 through S4. C2 evidence is produced within S5, so it is not a start condition. | -| R1 publication prerequisite. | R1 is merged. Its [evidence record](../../doc/project/evidence/model/wifi/r1-rate-publication.md) owns the parent comparisons. C2 must repeat notification checks with S3b's subscriber. | -| Before C2 implementation. | C2 uses the accepted individual-DATA purpose and scope. Complete HT-immediate transfer requires a separate A-MPDU design. Other C2 prerequisites retain their stated boundaries. The accepted fatal profile-conflict policy applies. | -| Before C2 individual-MPDU release is complete. | Implement the required clause. Resolve and review its [history and timing design](#interframe-history-and-timing-design) against the actual parent. Verify each condition through production reception without extra unsupported restrictions. | -| Before the C2 individual-MPDU release path is published. | The code reviewer accepts the history and timing design, and direct verification passes. Resolve any observation gap through the [escalation rule](#interframe-history-and-timing-design). A model approximation or reduced scope requires an explicit maintainer-approved plan revision. | -| Before C2 mismatch validation implementation. | Apply the accepted [fatal conflict policy](ieee80211-blockack-agreement-lifecycle.md#profile-conflict-detection-and-termination). Trace production BAR validation before state updates. Require exact diagnostic and compatible-run controls before publication. | -| Before C2's first production exchange or timing assessment. | R1 supplies committed local, BSS, and peer rates. Repeat publication callbacks with S3b's subscriber. HCF supplies actual BAR position. C2 supplies legal modes, timeouts, durations, and protection airtime. | -| Before C4 completes. | The migration fixture proves the default, warning limits, capability-gap fallback, and explicit Basic recovery for external-style configurations. | -| Before C3 or C4 proceeds. | The C2 owner records the first airtime result with its selected response mode and resulting admission decision under [Section 6](#6-verification-and-scope-limits). | -| Before S5 completes or the affected Compressed path is published. | The Compressed owner satisfies Section 6's final airtime conditions, including C3's checks for response modes and any required admission correction. | - -C2 belongs to S5. A new overrun outside Clause 10.23.2.9's permitted cases can require an admission correction before publication of the affected path. A permitted overrun requires its condition-specific evidence, not an admission correction. C3 repeats the assessment across supported response modes and exchange boundaries. The [airtime decision](#6-verification-and-scope-limits) controls continuation and completion; it does not require C2 results before S5 starts. - -### Source base and publication boundaries - -Local master at `633b071e213b825ce6019ff3e344ae49f52791ff` contains S1, R1, S3a, and the VHT/HT prerequisite from PR #1318. S1 merged through PR #1320 on 14 October 2026. Its original feature-series publication base is `ac6344cc5bcac972012759281a11b3f8255b5d46`. Original feature heads and comparison bases remain historical verification boundaries. Each remaining stage must pin its actual source parent and comparison base from the merged integration source. - -The lifetime and lifecycle plans retain their separately pinned `b06e8768` source observations. Before each prerequisite starts, its owner must refresh its source observations and caller assessment against the selected stage parent. This umbrella review does not replace either prerequisite's review. The decision table records accepted policy choices; stage reviews and verification remain required. - -S2 and S3b must refresh their prerequisite source observations against their actual selected parents. Those parents must include merged R1 and S3a; the refresh must not remain fixed to `1cdb362a`. Existing local S2 implementation does not establish merged S2 status or authorize a history change. - -A move to another base requires a fresh caller and test assessment. Shared paths identify overlap; they do not by themselves prove a merge conflict. This plan does not authorize branch mutation or publication. - -Each stage below has its own direct tests and regression evidence. Basic support remains available in every stage and in the final implementation. No prerequisite change claims completed Compressed support. - -The [stage conditions](#stage-conditions) define the remaining design, implementation, and verification obligations. - -### Implementation stages - -| Stage and purpose | Responsible components and change | Result and direct verification | -| --- | --- | --- | -| S1. Merged in PR #1320. | `ReceiveBuffer`, `BlockAckReordering`, and `RecipientQosMacDataService` count sequence positions and release complete groups in cyclic order. Valid Basic BAR creates a record before DATA. | Original-head verification passes eight recipient scenarios, three NAV runs, and 12 selected fingerprints. The [S1 record](#completed-s1-and-recorded-verification) states the supplied-agreement scope and approved baseline changes. | -| S2. Establish DATA lifetime. | Implement the [separate EDCA lifetime plan](ieee80211-edca-data-lifetime.md). Its owner supplies deadlines independently of Block Ack support. | Normal Ack DATA expires under either switch value. Identity transfer and accepted-exchange completion preserve ownership. | -| S3a. Merged. | HCF validates the selector with Basic as the temporary default. Legacy demonstrations and twelve existing fixtures select Basic explicitly. | The [S3a record](../done/ieee80211-compressed-blockack-s3a.md) reports direct Basic exchange and capability comparisons. Compressed selection still stops initialization. | -| S3b. Repair agreement lifecycle. | Implement A1, A2, A3a through A3c, and A4 of the [separate lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#5-reviewable-stages-and-dependencies). A2 distinguishes Basic compatibility from the Compressed HT requirement. Retain Basic fixtures and add focused Compressed admission checks. | Legacy ad hoc and non-HT consumers retain Basic. Compressed admission requires valid HT state; its capability-gap cases use Normal Ack. Shared lifecycle tests cover both profiles. | -| S4. Complete BAR work independently of DATA selection. | The originator policy, ACK handler, agreement window, HCF, and frame sequences isolate peer/TID state. They permit BAR-only contention without DATA extraction. Connect lifecycle stage A5. | One or four outstanding frames cause BAR below the default threshold of five. Another peer cannot suppress this work. Final completion causes DELBA. Compare changed Basic BAR timing with S4's parent before publication. | -| R1. Merged. | Full and simplified management commit local, BSS, and peer rates through the MIB. Combined operations commit related state before notification. | The [R1 record](../../doc/project/evidence/model/wifi/r1-rate-publication.md) reports publication, refresh, cleanup, and malformed-input checks. C2 later consumes these records and repeats subscriber checks. | -| S5. Add Compressed and activate its default. | Frame definitions, serializer, procedures, recipient scoreboard, response matching, rate selection, and protection support both profiles. C4 changes the default only after C2 and C3 pass. | Compressed exchanges pass direct bitmap, rate, and timing checks. Explicit Basic still supports type 0, fragment bitmaps, existing consumers, and its response contract. | - -S1, R1, and S3a are complete in the integration source. The remaining stages are S2, S3b, S4, and S5. A1, A2, and A3a through A3c can proceed without S2; A4 requires S2. Completed stages retain their separate parent comparisons. R1 satisfies the rate-publication prerequisite, but C2 still requires subscriber integration and legal rate consumption. - -S1's direct tests supply an agreement with a known buffer size through the existing Basic path. S3b later verifies that negotiation supplies the correct size to that repaired recipient. Lifecycle stage A4 depends on S2; A1, A2, and A3a through A3c need not wait for S2. S4 depends on S3b teardown and S1 recipient progress. - -The lifecycle plan owns the [A3 review split and publication conditions](ieee80211-blockack-agreement-lifecycle.md#5-reviewable-stages-and-dependencies). A3a covers directional teardown and inactivity; A3b covers association reset; A3c completes Action disposal and absent-agreement recovery. Required Action invalidation and safe completion accompany the first retirement or reset path that uses them. The split does not authorize an unsafe intermediate publication. - -S3a already precedes S3b in the integration source. S3b must use a parent that contains both S3a and R1. Preserve S3a's original parent comparison. Attribute later lifecycle effects to S3b. The final Compressed default takes effect at C4, after the runtime path and timing checks exist. +HCF owns coordination. The originator and recipient agreement handlers own role-specific state by peer and TID. The QoS data service selects DATA in queue service order. `InProgressFrames` holds extracted frames and exposes borrowed preview before extraction. -S5 depends on the completed prerequisite contracts. C1 changes wire definitions without a new rate-publication stage. C2 consumes merged R1 and repeats its interaction with S3b. S1's and R1's recorded parent comparisons remain separate from S4's future comparison. Unresolved C2 or C4 work does not reopen completed stages. +The ACK policy groups outstanding DATA by peer and TID within each accepted transmit window. HCF can request channel access for a due BAR without another DATA candidate. A matched response resolves the selected group. The agreement object owns cyclic sequence distance, bitmap membership, and transmit-window advancement. -### Completed R1 and S3a +Hypothetical example: the accepted window contains one sequence position and the BAR threshold is five. One outstanding DATA frame fills that window. The next candidate lies outside it, so HCF selects BAR despite the threshold. A positive response removes the acknowledged DATA and permits window progress. If no DATA remains at the safe exchange boundary, HCF starts completion DELBA. -R1 spans commits `c1973f443e` through `b385562353`. It adds combined MIB commits, production publication, membership-selector checks, and nonfinite-rate rejection. The [R1 evidence record](../../doc/project/evidence/model/wifi/r1-rate-publication.md) preserves its original source boundaries, commands, failures, and corrected results. Its final broad verification reports 46 runs; a later focused correction adds nonfinite-input checks. These recorded results do not establish a new combined-master pass. - -S3a spans `4e6aa75c99` and `633b071e21`. Its [execution record](../done/ieee80211-compressed-blockack-s3a.md) reports thirteen demonstration comparisons, eight capability probes, and nineteen fingerprint rows. Every calculated fingerprint ingredient equals its parent. Fifteen rows retain graphical `tyf` baseline mismatches, so the fingerprint runner exits 1. Four protocol fixtures retain expected FAIL results with unchanged causes. S3a changes no recorded baseline and proves no Compressed exchange. - -The inventories below remain preservation requirements for later stages. They describe the original S3a consumer set, not every fixture now present. Preserve the new selector, Basic observation, and R1 fixtures as additional controls. The original evidence belongs to its recorded feature source; this plan review runs no simulations. - -S1 changes recipient delivery, not the Compressed bitmap representation. S4 preserves Basic fragment rules; S5 retains those rules beside the Compressed restriction. Neither stage may claim a Compressed exchange solely because a helper handles a Compressed class. - -### S3a: demonstration migration and fallback inventory - -Merged S3a preserves the existing Basic demonstrations through explicit profile selection. The [Block Ack network](../../showcases/wireless/blockack/BlockAckShowcase.ned) and [TXOP network](../../showcases/wireless/txop/TxopShowcase.ned) retain their two ad hoc hosts. Later stages must retain this configuration scope. The plan requires no AP, Ethernet link, channel migration, or new association interval in either showcase. - -The current ad hoc management does not install peer HT records. That limitation prevents the Compressed HT profile, but it will not disable explicit Basic compatibility. The Basic profile preserves INET's existing capability assumptions. Its use with later PHY modes does not establish complete IEEE Std 802.11e-2005 conformance. - -Preserve S3a's explicit Basic assignment in each legacy configuration's existing scope: - -```ini -*.host*.wlan[*].mac.hcf.blockAckVariant = "basic" -``` - -Use the actual node patterns below for networks without `host*` names. HCF already validates the selector. Its temporary default remains Basic until C4 activates the completed Compressed implementation. A2 must revise the temporary rejection of Compressed, including the selector fixture's expected diagnostic. A2 must prevent an admitted setup from reaching the unsupported builder before C2 supplies that path. - -| Configuration and source | Retained behavior | Basic selection to preserve | -| --- | --- | --- | -| Block Ack [General](../../showcases/wireless/blockack/omnetpp.ini), `NoFragmentation`, `MixedTraffic`, and `Fragmentation` | Existing ad hoc topology, modes, radio settings, traffic, and fragment demonstration. | Select Basic on both hosts in General. All named configurations inherit it. | -| TXOP [General](../../showcases/wireless/txop/omnetpp.ini) | Existing ad hoc topology and `n(mixed-2.4Ghz)`; mixed Block Ack, Normal Ack, A-MSDU, and RTS/CTS exchanges. | Select Basic on both hosts. Retain thresholds and application settings. | -| Wireless QoS [MacQosWithBlockAck](../../examples/wireless/qos/omnetpp.ini) | Existing AP, two stations, two wireless hops, and inherited `g(mixed)`. | Select Basic on all participating interfaces in this configuration only. Do not change other configurations. | -| Ad hoc QoS [MacQos](../../examples/adhoc/qos/omnetpp.ini), runs 0 and 1 | Existing topology, `g(mixed)`, and support parameter loop. Run 1 retains Basic; run 0 remains support-disabled. | Select Basic on both hosts without a new parameter loop. | -| Fragmentation [HCFfragblockack](../../showcases/wireless/fragmentation/omnetpp.ini) | Existing Basic fragment exchange and comparison with the other fragmentation cases. | Select Basic for the participating interfaces in this configuration. | -| Fragmentation [parameterstudy.ini](../../showcases/wireless/fragmentation/parameterstudy.ini) | All four Block Ack variants and their existing parameter sweeps. | Select Basic within the shared Block Ack configuration scope or each named variant, according to inheritance. | - -Retain `PacketSizeHCFfragblockack`, `TxPowerHCFfragblockack`, `ThresholdHCFfragblockack`, and `OnlyPacketSizeHCFfragblockack`. Retain their names, result filters, analysis inputs, plot series, fingerprint rows, and speed records. - -S3a's completed migration covers six INI files and the ad hoc `${false, true}` parameter loop. Check effective INI inheritance before a later change to these assignments. Search tracked `tests/**/*.test` sources and shared includes separately. Generated files under `work/` do not replace those sources. - -For each legacy demonstration, require accepted ADDBA, Block Ack-policy DATA, Basic BAR, and a Basic response with positive receipt status. Check fragment status in the fragmentation cases. Record peer addresses and TIDs for every measured wireless hop. No peer HT record is required for Basic eligibility. - -Repeat those assertions after A2 and at final S5. A fingerprint match without a Basic exchange cannot establish preservation. Check the support-disabled ad hoc run separately. Add a focused Compressed ad hoc case with absent peer HT state. That case requires Normal Ack without ADDBA, BAR, or Block Ack. - -Preserve S3a's historical Basic explanation in the [Block Ack page](../../showcases/wireless/blockack/doc/index.rst), [TXOP page](../../showcases/wireless/txop/doc/index.rst), and [fragmentation page](../../showcases/wireless/fragmentation/doc/index.rst). State that Basic originated in IEEE Std 802.11e-2005 and is unsupported by IEEE Std 802.11-2024. Link to Section 2's historical and current-standard sources. Retain this distinction in affected QoS example descriptions and the parameter-study explanation. - -Preserve existing images and plots when the evidence still supports them. Shared recipient, lifetime, and lifecycle repairs can change results despite unchanged topology and traffic. Regenerate only affected evidence after a measured change. Keep prior measurements as historical evidence; do not claim unchanged fingerprints or current performance without verification. - -Existing infrastructure protocol fixtures will supply Compressed coverage through actual AP/STA capability exchange. This plan needs no showcase topology conversion or new IBSS capability implementation. - -### S3a: tracked fixture inventory and VHT scope - -S3a's recorded inventory covers tracked INI files, conditional assignments, included configuration files, and direct Basic consumers. Its original migration set contains twelve fixtures: eight protocol fixtures and four module fixtures. Five further module fixtures explicitly disable support. The separate S1 recipient fixture supplies accepted Basic agreements without an HCF enablement assignment. The response-mode unit fixture and serializer fixture also consume Basic types without that assignment. Later verification must include S3a's new selector and Basic observation fixtures in addition to this original set. - -The existing protocol fixtures include [_n.ini](../../tests/protocol/wifi/ini/_n.ini) or [_ac.ini](../../tests/protocol/wifi/ini/_ac.ini), followed by shared [_base.ini](../../tests/protocol/wifi/ini/_base.ini). Their [infrastructure network](../../tests/protocol/wifi/ned/WifiInfraNetwork.ned) uses full AP/STA management. The HT configuration selects `n(mixed-2.4Ghz)`; the VHT configuration selects `ac`. Association alone therefore does not prove that either endpoint has usable HT state. - -The [mode catalog](../../src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc) appends 128 existing HT combinations to `ac` and enables its HT-support flag. These HT modes use 5 GHz timing. [Ieee80211Mib](../../src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc) derives local capabilities from that catalog and operational limits. The [HT mode-set fixture](../../tests/unit/Ieee80211HtModeSet_1.test) requires `ac` to report HT operation. The [VHT/HT production fixture](../../tests/module/Ieee80211VhtHtSupport_1.test) checks peer HT state after association. Clause 4.3.15 defines a VHT STA as an HT STA with additional VHT features. - -The [S3a record](../done/ieee80211-compressed-blockack-s3a.md) records actual modes, local HT flags, peer HT state, and pre-A2 outcomes. S3b/A2 must retain Basic compatibility and check Compressed admission with real HT state. Before C2, admission fixtures do not claim a production Compressed exchange. C2 and C4 must retain the original `Ac_BlockAck` requirement with matched Compressed exchanges and positive receipt bits. Earlier generic frame-type passes establish neither negotiated-parameter correctness nor final Compressed support. - -This revision removes the proposed `Ac_BlockAck` deferral and its approval gate because their missing-HT premise no longer applies. The implementation must preserve `_ac.ini` and real capability exchange rather than replace VHT fixtures with HT-only configurations. If a later result exposes a different missing prerequisite, record its actual cause before any expectation proposal. The [recorded-expectation rule](../../doc/project/rule/testing.md#tr-baseline-deliberate) still governs such a proposal. - -The proposed `tests/protocol/wifi/11ac/Ac_BlockAckFallback.test` will supply an explicit absent-peer-state condition. Full and simplified management both install peer HT state on this branch. Under full management, a later valid [Beacon](../../src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc) can restore the STA's AP record. One-time removal therefore does not maintain the required gap throughout traffic. The fallback fixture will select simplified AP/STA management, which supplies static association without periodic Beacon refresh. Existing `Ac_BlockAck` will retain full management and real capability exchange. - -Use these fixture-local overrides with the existing `_ac.ini` and `_base.ini` includes: - -```ini -*.ap.wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified" -*.sta*.wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified" -*.sta*.wlan[*].agent.typename = "" -*.sta*.wlan[*].mgmt.accessPointAddress = "ap" -``` - -After all initialization stages, the fixture will remove peer HT records before the first DATA through `removePeerHtCapabilities`. It will remove the records at both ends of each measured wireless hop. Keep association active throughout measured traffic. Keep `opMode = "ac"`, local HT support, and Block Ack support enabled. - -Select the Compressed profile explicitly in the fallback fixture. Schedule no management restart, reassociation, or fixture action that reinstalls capabilities. Repeat this fallback check at A2, C2, and C4. - -The fallback assertions require delivered Normal Ack QoS DATA, absent peer HT state, and no ADDBA, BAR, or Block Ack during the measured interval. Check active association and valid local and peer HT state before the explicit removal. Check absent peer HT state at both ends during traffic and at the end of the measured interval. The fixture must start no agreement before removal. The result establishes fallback under a controlled capability gap with static association. It does not establish an on-air association exchange or a limitation of every `ac` station. - -Hypothetical example: full `ac` management installs valid peer HT state and A2 permits agreement setup. The fallback fixture first establishes static association through simplified management. It removes peer HT state after initialization, before DATA. No Beacon refresh restores that state, so A2 uses Normal Ack throughout the measured traffic. The positive and fallback fixtures must preserve those distinct initial conditions and results. - -The table assigns fixture changes to the stage that changes their contract. S3a recorded the original outcomes; those results do not establish future assertions. S3b's lifecycle owner owns capability and agreement adaptation. C2's production owner owns Compressed exchange assertions. C4's consumer owner owns default activation, profile-specific configuration validation, and coverage of both serializers. - -| Tracked fixture | Current assertion or dependency | Decision and stage | -| --- | --- | --- | -| [WifiQosDelayedBa](../../tests/protocol/wifi/common/WifiQosDelayedBa.test) | Delayed negotiation and response order; an existing deferred failure. | Select Basic explicitly and retain the deferred requirement. Add a separate Compressed initialization-rejection fixture at C4. | -| [Ac_MultiTidBa](../../tests/protocol/wifi/11ac/Ac_MultiTidBa.test) | Two TIDs and Multi-TID response; an existing deferred failure. | Retain its obligation and real `ac` management context. Record the first unmet requirement at each affected stage. | -| [N_Ampdu](../../tests/protocol/wifi/11n/N_Ampdu.test) and [Ac_Ampdu](../../tests/protocol/wifi/11ac/Ac_Ampdu.test) | A-MPDU subframe headers; two existing deferred failures. | Retain the aggregation assertions and HT/VHT capability context. Compressed support does not establish A-MPDU support. | -| [N_BlockAck](../../tests/protocol/wifi/11n/N_BlockAck.test) | Generic Action, BAR, and Block Ack frame types. | Before C2, use explicit Basic. C2 adds accepted ADDBA, matched Compressed frames, and positive bits. C4 checks the default. | -| [N_Amsdu](../../tests/protocol/wifi/11n/N_Amsdu.test) | `aMsduPresent`, without proof of accepted agreement. | Preserve that assertion. Add accepted-agreement permission checks at A2 and repeat with Compressed at C2. | -| [Ac_BlockAck](../../tests/protocol/wifi/11ac/Ac_BlockAck.test) | Generic Block Ack exchange under `ac`, currently passing. | Preserve positive Basic coverage before C2. C2 adds matched Compressed exchanges through real peer HT state; C4 checks the default. | -| [N_CompressedBa](../../tests/protocol/wifi/11n/N_CompressedBa.test) | A Basic class and positive Basic bitmap bit despite its name. | Keep its pre-C2 Basic reproduction. C2 changes this named Compressed fixture to actual Compressed frames and positive receipt assertions. Retain Basic coverage elsewhere. | -| [Ieee80211HcfHeader_1](../../tests/module/Ieee80211HcfHeader_1.test) | Ad hoc mode `a`, custom policy, and synthetic agreement. | Select Basic explicitly. Adapt initialized handlers for shared lifecycle changes without invented HT peer state. Preserve DATA/RTS and policy assertions. | -| [Ieee80211SingleProtectionGroup_1](../../tests/module/Ieee80211SingleProtectionGroup_1.test) | Group duration and no ACK-mode query, with support false and true. | Retain both controls. Explicit Basic preserves the existing support-enabled context; no successful agreement is claimed. | -| [Ieee80211RecipientNav_1](../../tests/module/Ieee80211RecipientNav_1.test) | Injected agreement and Basic BAR/response in ad hoc mode `a`. | Keep Basic types and explicit Basic selection. Add a separate Compressed case with valid HT state. Preserve NAV assertions in both cases. | -| [Ieee80211BlockAckReordering_1](../../tests/module/Ieee80211BlockAckReordering_1.test) | Eight S1 scenarios through the recipient service with supplied Basic agreements. No HCF profile parameter is present. | Retain all eight Basic scenarios through later stages. A2 adds negotiation checks through HCF. C2 and C4 add Compressed integration without removal of Basic assertions. | -| [Ieee80211HcfMultipleProtection_2](../../tests/module/Ieee80211HcfMultipleProtection_2.test) | Synthetic Basic frames and Basic duration calculations. | Keep explicit Basic cases and their duration assertions. Add Compressed duration cases at C2/C3; do not remove Basic coverage. | -| [Ieee80211ResponseModeSelection_1](../../tests/unit/Ieee80211ResponseModeSelection_1.test) | Basic BAR, 152-byte response, and current HT Basic response behavior. | Retain Basic assertions as model compatibility evidence. Add 32-byte non-HT Compressed cases, roles, peer isolation, and asymmetric state at C2/C3. Basic assertions do not establish current-standard conformance. | -| [Ieee80211VhtHtCapabilities_1](../../tests/unit/Ieee80211VhtHtCapabilities_1.test) | Local HT capability assembly from `ac`. | Retain positive capability assertions. Production Compressed fixtures must separately prove peer state and complete exchanges. | -| [serializer_chunk_roundtrip](../../tests/serializer/serializer_chunk_roundtrip.test) and [IEEE 802.11 fillers](../../tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc) | Existing Basic and Compressed BAR and Block Ack producers, with field inventories. | At C1, retain Type 0 Basic round trips and adapt Compressed reserved-field expectations. Verify complete Type decoding and ignored received reserved bits. C4 adds remaining coverage without removal of Basic producers. | - -S3b will adapt synthetic agreements through initialized handlers. Explicit Basic cases retain their existing capability assumptions, frame classes, and rates. Supply matching recipient agreement state when absent-agreement recovery requires it. Do not weaken NAV, policy, duration, or protection assertions to retain a pass. - -Before C2, modern-profile admission tests can exercise agreement policies without a Compressed DATA/BAR exchange. Capability-gap tests must refuse setup before the unsupported builder can run. Positive admission tests must isolate setup; C2 supplies the first complete production Compressed exchange. - -Five module fixtures explicitly disable support: `Ieee80211HcfMultipleProtection_1`, `Ieee80211HcfSingleProtection_1`, `Ieee80211PreparedCancellation_1`, `Ieee80211Txnav_1`, and `Ieee80211VhtHtSupport_1`. Retain these controls. They need no Basic migration or active agreement. Keep HT mode-set and VHT/HT capability unit coverage. +The [S4 verification record](ieee80211-compressed-blockack-s4-execution.md) covers the current final source. It does not establish a test result for every intermediate commit, every protocol fixture, or every configuration. ### R1: publish production rate state -R1 is merged and supplies production publication of local, BSS, and peer rates. The [R1 evidence record](../../doc/project/evidence/model/wifi/r1-rate-publication.md) owns its implementation contract and parent comparisons. C2 owns Compressed rate consumption. The requirements below remain regression obligations when later stages change these paths. - -Use the existing [MIB](../../src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h) stores and validation. The responsible management paths are: - -- `Ieee80211MgmtBase` publishes local legacy operational rates after a mode-set update through `setLocalRateSet`. -- Full AP management publishes BSS rates from its advertised elements through `setBssRateSet`. -- Full AP management installs peer rates from an accepted request when the successful association response exchange commits. -- Full STA management commits BSS and AP rates from accepted advertisements when association commits. -- Full STA management refreshes those records from valid associated-AP Beacons. -- Simplified AP and STA management publish equivalent rates during their existing static association operation. - -Use the merged combined MIB operations when related state commits together. `installBssAndPeerRateSets` commits rate records; `installBssAndPeerState` also commits peer HT state before notification. Preserve unknown state and a known empty basic-rate set as distinct values. Scan candidates and unsuccessful association attempts must not replace active rate state. - -AP departure removes that peer's rates without loss of BSS rates. STA association loss clears its old BSS and peer records. Existing management stop paths clear their corresponding records. R1 adds no stop/start recovery guarantee. - -Retain [Ieee80211MgmtRatePublication_1.test](../../tests/module/Ieee80211MgmtRatePublication_1.test) and [Ieee80211MgmtStateCommit_1.test](../../tests/module/Ieee80211MgmtStateCommit_1.test). These existing fixtures drive real management without direct rate injection. Compare published rates and basic flags with actual Supported Rates and Extended Supported Rates elements after affected changes. - -The fixture must cover these conditions: - -- Full AP/STA association, with observations at both endpoints. -- Simplified management publication, with equivalent static records. -- Valid associated-AP Beacon refresh and unrelated scan advertisements. -- Unsuccessful association without replacement of active records. -- Peer departure, association loss, and existing management stop paths. -- Unknown state and known empty basic-rate state. - -Retain focused Basic DATA, BAR, response-mode, and support-disabled controls. Current rate selectors do not consume these records; later rate consumers must preserve the retained Basic selection contract. Compare each later changed result with the responsible stage's pinned parent. Preserve R1's original comparison separately and record each new result's cause. - -R1 tests the MIB's notification contract without S3b's future subscriber. C2 must repeat that integration after both changes exist. A3b must use the merged combined-commit behavior instead of an assumed AP remove/reinstall notification pair. A rate-only update must preserve unchanged Basic and Compressed agreements. The built-in rate-control path must produce no additional reset or rate statistic solely from these notifications. External listeners retain their documented notification contract. - -C2 cannot replace R1 evidence with helper injection or a successful Compressed exchange. Preserve R1's recorded commands, mode, fixture selectors, seed, parent, and outcomes. Record later subscriber integration against its actual combined source. - -### Compressed replacement steps - -| Step and purpose | Owner and proposed change | Expected result and verification | -| --- | --- | --- | -| C1. Define valid type 2 frames beside Basic. | Add the Compressed builder and fixed 28-byte header chunk. Decode the complete Type field. Retain Basic classes, fields, builders, and producers within the Type 0 contract. Adapt existing Compressed round-trip producers and expected fields in this step. | Golden octets and round trips verify Type 0 and Type 2. Reject Basic values that encode another Type. Compressed reserved bits write zero and received values are ignored. Basic remains 152 bytes with FCS; Compressed remains 32 bytes. | -| C2. Connect complete Compressed exchanges. | Consume R1's committed rates. HCF derives BAR position and the recipient's actual interframe relation. Procedures and ACK handling select the configured profile and match the response variant. The record retains Compressed receipt truth after delivery and supplies the bit at `WinStartR - 1`. Add the individual-MPDU release condition without loss of Basic fragment behavior. Supply fragmentation suppression, Compressed rates, timeouts, durations, and protection airtime. | Real association supplies rate state. Production checks cover receipt retention, zero holes, interframe release, and ordinary gap retention. Empty records yield zero bits. Wrong-profile and malformed input cannot report success. Explicit Basic retains fragments and its response contract. Verify the accepted fatal conflict diagnostic; record the first Compressed timing result before C3 or C4. | -| C3. Verify both profiles and Compressed timing. | Add focused Compressed rate and timing cases. Retain the Basic response-mode fixture and its 152-byte assertions. Add 32-byte Compressed cases with directional queries, peer isolation, and ACK/CTS controls. | Primary and legal alternate Compressed rates produce correct duration. Initial and later BAR use legal rates. Profile-specific lengths remain distinct. Timing checks distinguish permitted overruns from unlawful continuation. | -| C4. Activate the default and complete compatibility guidance. | Set Compressed as the final default. Add the migration diagnostics and external-style fixture. Keep explicit Basic settings and complete both serializer inventories. Add Compressed delayed-policy rejection. | Default eligible HT links use Compressed. Default links without usable HT state use Normal Ack. Explicit Basic remains usable without HT peer records. No Basic class, showcase configuration, parameter-study variant, or result row is removed. | - -S5 consists of C1 through C4. Every commit must build. C4 follows runtime conversion and timing verification. The temporary Basic default in S3a prevents premature activation; the completed implementation defaults to Compressed as requested. - -Retain the existing Basic methods for external consumers. Add Compressed methods or a shared variant-aware operation only where the existing contract cannot serve both formats. Document new interface obligations. Retention avoids Basic class removal; it does not guarantee that every external implementation needs no adaptation. - -C2 adds all runtime operations for a supported Compressed exchange. C3 adds verification after that path exists. Basic fragment cases remain after C2 and C4. Ordinary fragmentation outside any agreement and rejected Compressed fragment input remain separate tests. - -Retain these six speed records from [tests/speed/store.json](../../tests/speed/store.json). Each inspected record has `run_number = 0`. - -| `working_directory` | `ini_file` | `config` | -| --- | --- | --- | -| `showcases/wireless/blockack` | `omnetpp.ini` | `Fragmentation` | -| `showcases/wireless/fragmentation` | `omnetpp.ini` | `HCFfragblockack` | -| `showcases/wireless/fragmentation` | `parameterstudy.ini` | `OnlyPacketSizeHCFfragblockack` | -| `showcases/wireless/fragmentation` | `parameterstudy.ini` | `PacketSizeHCFfragblockack` | -| `showcases/wireless/fragmentation` | `parameterstudy.ini` | `ThresholdHCFfragblockack` | -| `showcases/wireless/fragmentation` | `parameterstudy.ini` | `TxPowerHCFfragblockack` | - -Retain the Block Ack `General`, `MixedTraffic`, and `NoFragmentation` records and TXOP `General`. Match records by directory, INI file, configuration, and run number. Reassess measured effects through the applicable baseline procedure. Do not delete these records because Basic is unsupported by the current standard. - -Retain [Fragmentation.anf](../../showcases/wireless/fragmentation/Fragmentation.anf), its result inputs, filters, configuration order, and plot series. The inspected Block Ack and TXOP directories have no `.anf` files. Repeat the consumer search before implementation. The speed store has no records for either QoS example directory; their direct and fingerprint checks still apply. - -Documentation will label every retained Basic demonstration as historical compatibility. Give the IEEE Std 802.11e-2005 origin and IEEE Std 802.11-2024 exclusion with their references. The label must accompany the relevant user-facing explanation, configuration description, and analysis caption. Do not require a new topology image solely because the standard status note changes. - -Release notes and migration guidance will explain the new default and the explicit Basic compatibility setting. Compressed agreements suppress fragmentation; Basic agreements preserve it. The threshold still applies outside Compressed agreements. An HT PHY mode alone does not select the profile. - -A smaller wire-only prerequisite can add type 2 serialization tests. It cannot establish a complete Compressed exchange without the required agreement, scoreboard, rate, and response behavior. - -Keep `delayedAckPolicySupported` parameters and methods. Both currently default to false. The Compressed profile will reject true at initialization because this plan supplies no delayed Compressed exchange. Retain the existing deferred delayed test with explicit Basic selection. Basic retention does not make that missing behavior pass or authorize a new Basic initialization restriction. - -The choices have explicit owners: - -- The [lifetime plan](ieee80211-edca-data-lifetime.md#3-proposed-ownership-and-behavior) owns `msduLifetime` and its default. -- The [lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#4-standard-requirements-and-inet-choices) owns `addbaFailureTimeout`, its default, and discard-all. -- This plan owns `blockAckVariant`, its final Compressed default, and explicit Basic compatibility. -- This plan owns the fallback when the basic-rate set is unknown. - -The [decision table](#maintainer-decisions) records the accepted lifetime default and transfer-completion predicate. Basic retention does not remove their compatibility effects. Required measurements remain unexecuted. The 100 ms response timeout and fatal profile-conflict policy are also accepted. - -## 6. Verification and scope limits - -The [lifetime plan](ieee80211-edca-data-lifetime.md#5-verification-and-evidence) owns lifetime verification. The [lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#6-direct-verification) owns setup, Action, inactivity, teardown, and association checks. Its performance section owns setup-delay and disposal measurements. - -Retain these direct checks for recipient delivery, BAR channel access, and final Compressed support: - -- Rate publication, R1: Establish real AP/STA association without direct MIB rate injection. Require known local operational rates, BSS basic rates, and peer operational rates at both stations. Compare committed rates and basic flags with the actual Supported Rates and Extended Supported Rates elements. STA management must update rates after a valid associated-AP Beacon; scan candidates and unsuccessful association attempts must preserve active rate state. Check removal after association loss and equivalent publication through simplified management. A rate-only update must preserve an unchanged agreement in either profile. Absent HT state must not retire a Basic compatibility agreement. Repeat these subscriber checks at C2 after R1 and S3b both exist. - -- BAR classification, C2: Use BSS basic rates 6, 12, and 24 Mbps with peer operational rates that also include 54 Mbps. Exercise initial and continuation BAR through HCF. Require 24 Mbps for the initial BAR and 54 Mbps for an eligible later BAR. Repeat after fresh channel access and require initial classification again. Preserve RTS/CTS and ACK control assertions. Direct classification helper tests cannot replace the HCF path. - -- Basic wire contract, C1: Check golden Type 0 octets, lengths, and fragment bitmap format. Retain Basic field round trips that encode Type 0. Reject a Basic reserved value that changes the Type. Type 0 must deserialize as Basic. - -- Compressed wire contract, C1: Check golden Type 2 octets, lengths, and the 64-bit bitmap format. Adapt the current Compressed producers and expected fields with the serializer change. Check zero writes and ignored received values for B0 and B5–B11. Nonzero B3 or B4 changes the Type and cannot use that ignore rule. Check short frames and unsupported types 1, 3, and 4 through 15. - -- Scoreboard validation, C2: Require zero bits for an empty scoreboard under an active agreement. A Basic frame under a Compressed agreement must fail profile validation before receipt-state changes. - -- Compressed response validity: Inject matching Type 2 responses with nonzero Fragment Number, incorrect bitmap size, or incorrect length. Exercise serialized and directly supplied typed input. Require failed exchange recovery without acknowledgment, inactivity refresh, or success feedback. - -- Receipt retention, C2: Drive DATA 100, 102, and 101 through the production recipient path under an accepted Compressed agreement. Require delivery in order 100, 101, and 102. Send BAR at 100 and require all three receipt bits after delivery. Retain Basic fragment checks separately. - -- Scoreboard holes, C2: With agreement start 100 and size eight, receive only DATA 115. Require scoreboard window 108–115, a positive bit only for 115, and zero bits for 108–114. Repeat across sequence wrap. These checks must reject the Basic getter's inferred positives for absent earlier positions. - -- Recipient fragments: Retain S1 scenario 0 to check Basic capacity and complete-unit delivery through the recipient path. At C2, inject fragmented DATA under an active HT-immediate agreement. Use nonzero Fragment Number or More Fragments set. Require no receipt state update, positive bitmap bit, fragment state, or DATA delivery. These checks prohibit fragmented Compressed reception; the originator checks below prohibit fragmented transmission. Repeat S1's Basic fragment case at C2 and C4, with fragment status and complete-unit delivery retained. - -- Originator fragmentation: With an active HT-immediate agreement, extract an MSDU above the configured fragmentation threshold. Check one unfragmented DATA frame, including when the ACK policy selects Normal Ack. Without an agreement, check ordinary fragmentation and Normal Ack. Start setup while numbered fragments remain and check that all resolve before ADDBA transmission. Verify that the first DATA under the new agreement is unfragmented and uses the accepted request number. - -- Basic BAR timing, S4: Compare with S4's pinned parent before publication. Test standalone BAR, final BAR after one or four DATA frames, and late continuation near a positive TXOP limit. Exercise single and multiple protection. Record modes, sizes, IFS, response airtime, completion time, and the Data/Management count. - - Check applicable TXNAV and TXOP bounds with each permitted-overrun condition explicit. Separate inherited violations from violations introduced by S4. Resolve a new unlawful overrun before publication of that affected Basic path. +The MIB owns local, BSS, and peer rate and HT facts. Management publishes coherent state through the combined MIB commit operations. Rate consumers read that committed state. Association reset uses a separate management-to-MAC call because an unchanged capability value does not identify a new association. The [reset contract](ieee80211-blockack-agreement-lifecycle.md#reassociation-consumers-and-callback-order) explains the cleanup order. -- Airtime and TXOP: At C2, repeat affected S4 checks with Compressed exchanges and compare with C2's pinned parent. Test above-threshold DATA near a positive TXOP limit with Block Ack enabled. Record DATA and response modes, sizes, IFS, protection, response airtime, and completion time. Record the Data/Management count and each aggregate format within that TXOP. +## 4. DATA selection and BAR operation - Check containment or the exact permitted-overrun condition under Clause 10.23.2.9. Measure Compressed timing through its infrastructure fixtures. Check retained Basic showcase airtime separately, with both wireless QoS hops and the current ad hoc hops. Separate permitted overruns, inherited violations, and new violations before the C3 or C4 decision. +Preview borrows a candidate without queue removal or sequence assignment. For an unextracted candidate, the data service reports the prospective sequence number. The original frame retains its header and queue ownership. The [preview interface](../../src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h) exposes these operations. -- Final response timing: At C3, repeat affected airtime checks across primary and legal alternate response modes implemented by C2. Include the lowest supported rates and late continuation near the TXOP limit. Test a permitted initial unaggregated MSDU under Block Ack, an initial A-MSDU, and a later exchange that cannot finish within the required bound. Require containment for the A-MSDU and applicable later exchange. A permitted initial overrun must not create a false admission prerequisite. A C2 result covers only its recorded modes and exchange boundaries; a fingerprint match does not prove timing compliance. +Below `blockAckReqThreshold`, the ACK policy defers a group's BAR only when the next eligible DATA belongs to that group and fits its window. An empty queue, another peer or TID, or an out-of-window candidate can therefore make BAR due. Counts from different groups do not combine to meet the threshold. -- Demonstration coverage: Preserve S3a's explicit Basic selection and recorded direct assertions from Section 5. Repeat the assertions after A2 and at C2/S5. Every legacy Block Ack demonstration must retain actual Basic exchanges, including ad hoc QoS run 1. Retain run 0 as the support-disabled control. Check Compressed capability-gap fallback separately; it must not replace legacy coverage. +The frame sequence selects BAR before DATA extraction. Basic Block Ack response acceptance checks the local receiver, requested peer, and requested TID. Transmitted-BAR processing limits acknowledgment-state changes to the request's peer, TID, and bitmap range. The response starting sequence number alone does not advance the transmit window. -- Fixture coverage: S3a records the [tracked fixture inventory](#s3a-tracked-fixture-inventory-and-vht-scope), including HT/VHT state and actual failure causes. S3b runs the capability, agreement, A-MSDU permission, and fallback checks before final conversion. C2 requires Compressed responses in retained positive Block Ack cases. C4 runs the final inventory, serializer expectations, and delayed-policy rejection fixture. Report deferred protocol obligations separately from successful exchanges and successful fallback checks. +BAR collision and failure update EDCA contention state without a DATA retry entry. Failed Block Ack feedback permits DATA retransmission with Retry set. Resolution and lifetime disposal permit the agreement window to advance, without a jump over unresolved DATA. -- Recipient gap release, completed S1: Retain scenarios 1 and 2 of the recipient fixture. Scenario 1 buffers complete DATA 101 and 102 behind absent DATA 100. Receipt of DATA 100 must release all three units in order in that operation. Scenario 2 repeats this condition across sequence wrap. Retain their assertions for delivery order and the resulting reorder window without a later DATA reception or BAR. +## 5. Configuration and compatibility -- Recipient interframe release, C2: Supply an agreement with start 100 and size four through HCF. Receive Normal Ack DATA 100 and observe its ACK completion. In a zero-propagation control, start DATA 104 reception one SIFS after local ACK transmission ends. Require adjusted scoreboard start 101, receipt evidence for 100, immediate delivery of 104, and reorder start 105. - - Use actual receive times and the transmission callback. A directly supplied relation flag cannot replace this production check. Repeat with applicable SIFS and RIFS relations after a previous MPDU. Repeat the positive case across sequence wrap. - -- Recipient interframe controls, C2: Repeat with a false bit at `WinStartR - 1` and an interval outside the accepted timing relation. Each control must retain 104 behind the gap. Add nonzero propagation delay and intervening unrelated reception or transmission. Test history invalidation after agreement retirement, peer cleanup, and replacement setup. Check that stale history cannot release DATA. Record exact receive and transmit endpoints and the applied timing allowance. Retain Basic controls and explain each changed Compressed fingerprint through observed delivery events. - -- Recipient BAR before DATA, completed S1: Retain scenario 3 with supplied agreement start 100 and buffer size eight. Before DATA, valid Basic BAR at 104 must create the record and set reorder start 104. Subsequent DATA 104 must reach immediate delivery. Invalid BAR and BAR without an agreement must create no reorder record. - -Repeat both recipient cases with Compressed frames at C2 and C4. Each result must preserve the repaired delivery order and window state. - -- Recipient window and BAR: Test DATA beyond `WinEndB` after an older DATA drop. Check its delivery, scoreboard bit, and released older complete units. With DATA 100 absent and DATA 101 buffered, send a valid BAR at 101. Check delivery of 101 and independent scoreboard movement. Release 4095 and 0 in one operation and assert that exact order at the next MAC process. An invalid BAR must leave both windows unchanged. - -- BAR originator: Test BAR-only contention, response matching, timeout, older retries, and window advance after acknowledgment or drop. With single protection enabled, check that BAR selection and duration calculation do not extract or number queued DATA. Test two peers with the same TID and a BAR for only one peer. Under an established agreement, test one and four DATA frames with the default threshold of five. Keep peer B continuously eligible while peer A has four outstanding frames. Check that A receives BAR before another DATA for B. - -- Originator identity, S4: Give one peer outstanding DATA for TIDs 2 and 5. Check separate threshold counts and BAR selection for each TID. Transmit BAR for TID 5. Require no ACK-state change for TID 2. Repeat these checks with Compressed frames at C2 and final S5. - -- Small originator windows, S4: Accept buffer sizes one and four with BAR threshold five. Require BAR before DATA beyond each transmit window. Repeat these checks with Compressed frames at C2 and final S5. - -- Earlier Compressed response start, C2: Use transmit window 100 through 163 and BAR start 104. Let transmitted DATA 102 remain unacknowledged without a wait for that BAR. Receive a matched response with start 100 and a positive bit for DATA 102. Require acknowledgment of 102 through HCF. Positive bits for unsent frames or frames outside the active window must leave those frames unacknowledged. - -- Later or disjoint Compressed response, C2: Send a matched response whose start follows some requested frames. Require unchanged acknowledgment state below that start. Require those frames to leave their BAR wait and become retry eligible. Send a matched response without overlap. Require every requested frame to remain unacknowledged and become retry eligible. - -- DATA lifetime integration: Repeat zero-bit Compressed responses and BAR timeouts until the existing DATA deadline expires. Check sequence preservation, Retry-bit policy, disposal, and window progress. Let the deadline expire during an accepted Compressed exchange. Check safe completion and absence of another DATA attempt through the lifetime owner's contract. - -- Response rates, C3: Test the highest qualifying BSS basic rate and the mandatory-rate fallback when no basic rate qualifies. Test the documented INET fallback when the basic-rate set is unknown. For HT and VHT BAR inputs, assert the non-HT reference rate and the applicable OFDM or ERP-OFDM modulation class. For non-HT BAR, assert the received modulation class and preamble type. Test a legal alternate response rate with equal duration and an invalid override. Observe the transmitted response mode and Duration/ID through HCF. - -- Originated BAR rates, C3: Require a non-HT PPDU for initial and later Compressed BAR. Give the BSS and peer different legacy rate sets. Check the initial basic-rate rule and the later peer-operational-rate rule. Check fallback with unknown peer state, an empty basic-rate set, and unknown BSS rate state. Reject a configured HT/VHT BAR mode or a legacy rate that fails the applicable rule. Preserve unrelated RTS, CTS, ACK, and DATA mode checks. - -- Migration diagnostics, C4: Use the [external-style migration fixture](#migration-diagnostics), outside the explicit legacy override inventory. Check local non-HT warnings, HT ad hoc fallback reasons, and explicit Basic exchanges. Disabled support must produce no irrelevant warning. Keep Compressed as the selected default. - -- Profile conflict, C2: Send a valid addressed BAR of the other variant to an active peer/TID. Require the fatal configuration diagnostic before agreement, scoreboard, reorder, response, or inactivity updates. Check both mismatch directions and no conflict-triggered DELBA. Require termination at the first qualifying BAR. Verify diagnostic fields and cause, not merely an expected failure status. In separate compatible runs, verify Basic and Compressed exchanges plus unaffected Normal Ack traffic. Keep malformed, unknown-variant, unaddressed, and absent-agreement cases separate. - -- Compatibility and activation: Default eligible HT links must use Compressed. Explicit Basic must preserve legacy classes, bitmap fragments, response lengths, and existing NAV and protection assertions. Change `N_CompressedBa.test` to require Compressed and retain positive `Ac_BlockAck` coverage through real HT state. Keep Basic NAV and multiple-protection scenarios; add Compressed scenarios rather than removal of Basic coverage. Extend response-mode tests with 32-byte Compressed cases beside 152-byte Basic cases. Retain directional queries, peer isolation, asymmetric state, and ACK/CTS controls. - -The [standalone lifetime tests](ieee80211-edca-data-lifetime.md#5-verification-and-evidence) own identity, aggregation, fragmentation, and IFS deadline checks. Run each prerequisite's tests against its Basic path before its replacement. Repeat affected integration checks with Compressed frames at S5. - -Use direct production assertions for peer, TID, response timing, bitmap, channel access, agreement state, and queued-frame outcome. A unit bitmap test alone cannot prove HCF uses the result. - -Add a proposed `tests/unit/Ieee80211CompressedBlockAck_1.test` for wire and scoreboard checks. Add a proposed `tests/module/Ieee80211HcfCompressedBlockAck_1.test` for the production exchange. - -Use the HCF module fixture for identity, small-window, shifted-response, response-mode, and profile-mismatch cases. - -- Test the default Compressed setting and explicit Basic at both endpoints. -- Test unknown selector values at initialization. -- Give two HT peers different profiles. -- Require no cross-variant acknowledgment. A valid addressed BAR that conflicts with an active agreement must cause the exact fatal configuration diagnostic before state updates. Require no conflict-triggered DELBA or repeated setup. Verify compatible traffic in separate runs; absent-agreement recovery retains its separate DELBA checks. - -Retain [Ieee80211BlockAckReordering_1.test](../../tests/module/Ieee80211BlockAckReordering_1.test) through the later stages. Its current scenarios 0 through 7 assert delivery and state through `RecipientQosMacDataService`. Scenario 0 covers fragment capacity; scenarios 1 and 2 cover gap release and wrap; scenario 3 covers BAR before DATA. Scenarios 4 through 7 cover BAR order, window movement, peer/TID isolation, and A-MSDU order. The fixture supplies accepted Basic agreements. A2 must add negotiation checks through HCF, while C2 and C4 must verify Compressed integration. - -After the required fresh debug build, use this current selector from the checkout root. The fixture selects all eight recipient scenarios and seed 0 internally. The command also selects the three current NAV runs. This document update does not execute the command. - -```sh -inet_run_module_tests -m debug -f '(Ieee80211BlockAckReordering_1|Ieee80211RecipientNav_1)\.test$' -``` - -Run those exact cases with the unit and module runners in debug mode. Select all eight current protocol fixtures. Add the proposed absent-peer-state `ac` fallback fixture after it exists. Select the four Block Ack-enabled module fixtures and five support-disabled controls. Include the separate S1 recipient fixture with all eight Basic scenarios. - -Include the HT mode-set, VHT/HT capability, and response-mode unit fixtures. Add the proposed delayed-policy rejection fixture at C4. Run the serializer fixture with its suite's runner when C4 changes its expected fields. A fixed seed suffices for these deterministic cases. - -For each stage, record selected paths and runs, expected and observed outcomes, and the first failed assertion or initialization error. Preserve source includes and generated configurations with the evidence. Distinguish an expected protocol failure from generation, compilation, or unrelated runtime failures. Keep the four current deferred protocol requirements separate from positive `Ac_BlockAck` and absent-peer-state fallback coverage. Appendix A separates completed S1 results from the earlier capability results. Checks for the later stages stay proposed. - -Use the common selection from `tests/fingerprint/showcases.csv` and `tests/fingerprint/examples.csv` through final C4. Retain all 19 recorded rows. Preserve each row's configuration, run number, time limit, and fingerprint ingredients. Explicit Basic settings prevent the default change from converting these legacy cases. Shared repairs can still change results; record their causes rather than an assumed unchanged baseline. - -The four wireless QoS rows exercise shared HCF callers, including `MacQosWithBlockAck`. The two ad hoc QoS rows separate the support-disabled control from retained Basic compatibility after A2. The TXOP row preserves its intended mixed frame exchanges through topology and protocol changes. Block Ack-disabled EDCA rows check regression effects within S2's broader lifetime scope. S2 must also satisfy the lifetime plan's [measured-expiry requirement](ieee80211-edca-data-lifetime.md#5-verification-and-evidence). DCF rows serve as controls for shared changes. - -| Simulation path | Retain after C4: 19 rows | +| Setting or contract | Current behavior | | --- | --- | -| `showcases/wireless/blockack` | `NoFragmentation`, `MixedTraffic`, `Fragmentation` | -| `showcases/wireless/aggregation` | `NoAggregation`, `Aggregation`, `VoicePriorityAggregation` | -| `showcases/wireless/fragmentation` | `DCFnofrag`, `DCFfrag`, `HCFfrag`, `HCFfragblockack` | -| `showcases/wireless/qos` | `NonQos`, `Qos` | -| `showcases/wireless/txop` | `General`; run 0 | -| `examples/wireless/qos` | `MacQos`, `MacQosWithoutAggregation`, `MacQosWithRtsCts`, `MacQosWithBlockAck`; run 0 | -| `examples/adhoc/qos` | `MacQos`; runs 0 and 1 | - -The TXOP row records five seconds; each ad hoc QoS row records ten seconds. These limits come from the fingerprint CSV entries, not the INI defaults. Block Ack `General` has no separate fingerprint row; its S3a checks include direct exchanges and its retained speed record. - -Record the exact selected rows before execution. Compare each stage with its pinned parent before later stages change the same paths. Extend the selection when changed callers or a failure justify more cases. Diagnose the first changed exchange in each affected case before any baseline proposal. - -Record a cause for each changed result at the stage that introduces it. A stage need not change every selected fingerprint. Preserve raw results and the selected parent revision. Fingerprints check regression effects; direct production assertions establish the required behavior. S1's four approved changes remain recorded under its [separate authorization](../done/ieee80211-compressed-blockack-s1.md). This plan revision authorizes no additional baseline update. - -Follow [run-the-gates.md](../../doc/project/guide/run-the-gates.md) for builds and checks. Each later implementation will record exact commands, modes, selectors, and outcomes. Appendix A records completed S1 and earlier capability verification. Checks for the later implementation stay unexecuted. - -Basic frame classes and existing builders remain public. The implementation will update user and migration guides, release notes, configuration descriptions, analysis captions, and serializer coverage. State that Compressed is the default and `blockAckVariant = "basic"` selects historical compatibility. Cite IEEE Std 802.11e-2005 as the origin and IEEE Std 802.11-2024 as the standard that reserves type 0. Do not claim current-standard support for Basic or complete historical conformance for existing INET behavior. - -Document added interface obligations without removal of Basic contracts. Explain `addbaFailureTimeout`, per-EDCAF `msduLifetime`, setup deferral, final-transfer DELBA, and discard-all under their separate owners. Preserve individual-DATA and stop/start limitations. No existing showcase topology or configuration name needs migration under this decision. - -The [lifecycle plan](ieee80211-blockack-agreement-lifecycle.md#3-agreement-behavior) owns recovery after rejection, uncertain setup, and failed DELBA for both profiles. Normal Ack fallback due to absent HT state belongs to Compressed. Basic compatibility must not silently become that fallback when the default changes. - -Release notes must retain positive Compressed coverage for eligible `ac` peers and the four existing deferred protocol requirements. Explain originated Compressed BAR's non-HT mode and `controlFrameBitrate` validation. Keep complete VHT transfer support outside the claim. +| `isBlockAckSupported` | Defaults to false. | +| `blockAckVariant` | Defaults to `"basic"`; `"compressed"` fails initialization. | +| `msduLifetime` | Defaults to 0.512 seconds per EDCAF and requires a positive finite value. | +| `addbaFailureTimeout` | Defaults to 100 ms after request acknowledgment; it does not bound contention or DELBA recovery. | +| Completion DELBA | Applies at a safe exchange boundary after first DATA transmission, with no queued or numbered DATA. | +| Originator teardown | Discards transmitted unresolved DATA and preserves sequence continuity for surviving unsent DATA. | +| Recipient teardown | Delivers complete buffered units before resource release. | +| Association reset | Discards old peer DATA and receive state without delivery into the replacement association. | -The table gives normative IEEE Std 802.11-2024 evidence for Compressed and shared repairs. Section 2 gives the historical IEEE Std 802.11e-2005 references. Shared repairs do not establish complete Basic conformance to either edition. [Appendix B](#appendix-b-standards-source-notes) retains canonical locators and extraction limits. +Custom QoS data services must expose borrowed preview and prospective sequence lookup. Custom BAR policies must accept explicit agreement context. Omitted context arguments fail compilation. The [migration section](../../doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst) defines the extension obligations. -| Decision | Clause or table | Physical PDF pages | -| --- | --- | --- | -| Fragmentation boundary for HT-immediate agreements | 10.2.7 | 1882 | -| VHT stations retain HT operation | 4.3.15 | 356–357 | -| Initial and later BAR transmission rates | 10.6.6.2; 10.6.6.4 | 1943–1945 | -| Non-HT responses to non-HT, HT, and VHT BAR; legal alternate rates | 10.6.6.5.2; 10.6.6.5.4 | 1945–1947; 1950 | -| HT/VHT non-HT reference-rate calculation | 10.6.11 | 1962–1963 | -| Additional exchange bounds and permitted TXOP overruns | 10.23.2.8; 10.23.2.9 | 2010–2014 | -| Compressed retransmission | 10.25.6.7.1 | 2068–2070 | -| Retry for individual DATA; scope of the QoS retry rule | 9.2.4.1.6; 10.23.2.12.1 | 665; 2016–2017 | -| Positive bitmap bits within the transmit window, relative to the received Block Ack start | 10.25.6.8 | 2071–2073 | -| Consecutive delivery when DATA fills a reorder gap | 10.25.6.6.2.1(a)(2) | 2065–2066 | -| Individual-MPDU delivery with SIFS/RIFS and receipt evidence at `WinStartR - 1` | 10.25.6.6.2.1(a)(2)(v) | 2065–2066 | -| Full-state receipt retention and zero status for absent positions | 10.25.6.3 | 2060–2061 | -| Reorder window movement after BAR, including an empty window | 10.25.6.6.3.1 | 2067 | -| HT capability advertisement in IBSS beacons is a valid alternative | Table 9-62, row 33 | 781 | +### Selected-extraction contract -Table 9-62 requires HT Capabilities when `dot11HighThroughputOptionImplemented` is true and the station is not a 6 GHz station. The condition does not exclude IBSS. The choice to defer IBSS capability support remains an INET scope decision. +The data service uses ordinary dequeue for individual extraction of the advertised next frame. Filtered `canPullPacket()` and `pullPacket()` operations permit eligible DATA behind a deferred frame while the queue owner applies normal pull effects. Aggregation validates candidate eligibility and A-MSDU permission before extraction. Invalid aggregate candidates cause individual extraction of the selected eligible frame. -The prerequisite plans own [lifetime evidence](ieee80211-edca-data-lifetime.md#2-current-source-and-standard-support) and [lifecycle evidence](ieee80211-blockack-agreement-lifecycle.md#8-standards-evidence-and-scope-limits). Fatal profile-conflict termination is a model choice under the [lifecycle contract](ieee80211-blockack-agreement-lifecycle.md#profile-conflict-detection-and-termination), not a new requirement inferred from Clause 11.5.4. +This implementation uses the current queue API. It defines no separate optional selected-extraction interface. External queue implementations must preserve service order, ownership, residence metadata, pull events, and compound accounting through their queue operations. -### Airtime acceptance and admission dependencies - -The current branch excludes Block Ack-enabled HCF from complete-cost single-protection TXOP admission. [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) selects that admission path only when `isBlockAckSupported` is false. [HcfFs](../../src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc) rejects continuation when no TXOP time remains. That check does not prove that the complete next exchange fits. This plan does not add complete-cost admission for Block Ack. - -S4 introduces Basic BAR paths before Compressed integration. Its owner must assess those changed exchanges against S4's parent before S4 publication. A standalone BAR can qualify for a Control-frame overrun exception. A final BAR after several DATA frames cannot use the one-Data-or-Management-frame exception. Hypothetical example: four DATA frames leave 20 microseconds, but their final BAR exchange needs 80 microseconds. S4 must not introduce that unlawful continuation without an admission correction or a verified scope restriction. - -S4's record owns violations introduced by S4. The C2 comparison cannot relabel such a violation as acceptable inherited behavior. An inherited violation does not itself require this plan to repair every existing TXOP path. Each stage must still prevent new violations within its supported change. C2 and C3 repeat the assessment for Compressed integration and its supported response modes. - -At a fixed PHY mode, an unfragmented DATA frame takes longer than one constituent fragment. The total airtime can still be lower than the complete fragment sequence because fewer headers and responses are necessary. Hypothetical example: a previous DATA exchange ends with 100 microseconds left, but another DATA exchange needs 200 microseconds. The positive remaining interval does not authorize that continuation. Clause 10.23.2.8's applicable TXNAV bound and Clause 10.23.2.9's TXOP restriction still apply. - -An overrun alone does not establish a violation. Clause 10.23.2.9 permits specified overruns when the TXOP contains at most one Data or Management frame. These cases include initial transmission of an MSDU under a Block Ack agreement, outside an A-MSDU or a multi-MPDU A-MPDU. The clause also permits specified retransmissions and Control frame transmissions under its stated conditions. The timing record must identify the exact exception and every applicable condition. It must also check any independent TXNAV requirement. - -Hypothetical example: the first unaggregated MSDU under an agreement needs 200 microseconds within a 100-microsecond TXOP limit. The TXOP contains no other Data or Management frame. Clause 10.23.2.9 permits that initial-transmission case under its stated aggregate conditions. The same example with an initial A-MSDU does not qualify. A second DATA exchange also defeats the one-Data-or-Management-frame condition. - -No current evidence proves timing compliance for final Compressed operation or the repaired legacy demonstrations. C2 must record its first timing result before C3 or C4 proceeds. Each result must distinguish permitted overrun, inherited violation, and new violation. A permitted overrun creates no admission-correction prerequisite. - -For a new violation in a supported case, the affected stage requires a correction through the separate TXOP design before publication. Apply that condition to S4's changed Basic paths and C2's changed Compressed paths. C3 repeats the Compressed classification across response modes and exchange boundaries. An inherited violation does not justify a new violation. - -If S4 evidence establishes a required correction, its affected Basic path cannot publish until that correction passes its direct checks. S5 likewise cannot complete while a required Compressed correction remains absent or unverified. A new unlawful continuation can block its dependent stage. A permitted initial overrun cannot. Record the blocked path, violated clause, required correction, and comparison parent in the responsible stage's timing result. - -A restriction on supported configurations is an alternative only when direct evidence proves timing compliance throughout its stated scope. Do not select a fixed MSDU size cap as an assumed solution. Hypothetical example: a capped second DATA exchange needs 80 microseconds, but only 50 microseconds remain. The cap does not prevent that unlawful continuation. PHY rates, protection, responses, and the remaining interval all affect the required bound. No size cap or silent Normal Ack fallback is an automatic recovery policy in this plan. - -## Appendix A. Executed evidence - -### Completed S1 and recorded verification - -S1 merged through PR #1320 on 14 October 2026 at `1cdb362a2a4b97f564566487079d9d156a5d213d`. Its original verified feature head is `fcc06450355a52b9c8b0f8f2647e0654711bfaca` on `work/compressed-blockack-s1`. Its original publication base is `ac6344cc5bcac972012759281a11b3f8255b5d46`. The [S1 source record](../done/ieee80211-compressed-blockack-s1.md) supplies scope, reproduction, and approved fingerprints. Its [human summary](../done/ieee80211-compressed-blockack-s1-summary.md) explains recipient behavior and caller requirements. The results below retain their original source boundaries and establish no fresh runtime verification at the merged revision. - -The [published PR description](https://github.com/inet-framework/inet/pull/1320) records verification at each original source boundary. The local final-series audit supplied additional driver commands and raw result locations. That untracked artifact is absent from this worktree. Use the PR description and committed S1 records for available evidence. This plan update ran no new build, module test, simulation, or fingerprint comparison. - -| Recorded check | Result | Scope limit | -| --- | --- | --- | -| Debug and release builds at each commit. | All recorded builds pass. | Release behavior tests remain unrun. | -| Debug recipient fixture at the final head. | All eight scenarios pass through the production recipient service. | Agreements and buffer sizes are supplied; ADDBA negotiation and Compressed exchanges remain outside this evidence. | -| Debug recipient NAV fixture at the final head. | The `hcf`, `dcf`, and `blockAck` runs pass. | The Basic HCF exchange does not establish negotiated capacity or recipient DATA delivery. | -| Debug fingerprint selection at the final head. | All 12 selected configurations pass with the approved S1 values. | This selection differs from the proposed 19-row common inventory; it does not verify that full inventory. | - -The recorded module driver uses `opp_repl` and `run_opp_tests()` for `tests/module` in debug mode after the library build. Its filter is `(Ieee80211BlockAckReordering_1|Ieee80211RecipientNav_1)\.test$`. Both fixtures use seed 0. The recipient fixture selects scenarios 0 through 7 at the original final head. The NAV fixture selects `hcf`, `dcf`, and `blockAck`. These are recorded selectors and results, not fresh execution at the merged revision. - -The fingerprint selection uses the command below from `tests/fingerprint`. The recorded final invocation returns exit status 0. Each selected row uses run 0, its CSV time limit, and the default seed set. The command is recorded evidence, not a new execution by this document update. - -```sh -./fingerprinttest -d -t 4 \ - -m 'showcases/wireless/(blockack|fragmentation|aggregation)/|examples/wireless/qos/.*-c (MacQosWithBlockAck|MacNonQos)' \ - -f tplx -f '~tNl' -f '~tND' showcases.csv examples.csv -``` - -The delivery commit `53130418de` owns four baseline changes under the maintainer's separate approval. Each first changed operation releases additional complete units when DATA fills a gap. The [S1 source record](../done/ieee80211-compressed-blockack-s1.md) gives exact times and fingerprint values for these configurations: - -- `showcases/wireless/blockack:NoFragmentation`, run 0. -- `showcases/wireless/blockack:MixedTraffic`, run 0. -- `showcases/wireless/fragmentation:HCFfragblockack`, run 0. -- `examples/wireless/qos:MacQosWithBlockAck`, run 0. - -The capacity and BAR commits change no baseline. The final series preserves Basic frame formats, fragment bitmaps, and the ordered `ReleasedFrames` contract. The protected compatibility lookup remains deprecated for one release, as the migration guide states. S1 establishes no lifetime, lifecycle, rate publication, Compressed exchange, or performance result. - -### Branch verification record - -The results below are historical evidence from the VHT/HT branch before S1, the proposed selector, and the Compressed default. They retain their original source and scope limits. The separate S1 record above describes the completed recipient repair. This document revision adds no execution result. - -The review executed these checks on 13 October 2026 at source revision `6aefaef1f01a70134fa8283de708353d4a6d4fd8`. The working directory was `/home/user/omnetpp_ws/inet-ieee80211-vht-ht-support`. The build and all three runner commands returned exit status 0. The fixtures use `General` with seed 0. The module fixture runs scenarios 0 through 6. - -| Executed check | Observed result | Scope limit | -| --- | --- | --- | -| `make MODE=debug -j8` | The debug build passed and reported no further source build work. | This build precedes the proposed implementation. | -| Unit filter `Ieee80211(HtModeSet\|VhtHtCapabilities)_1\.test$` | Both fixtures passed. They require HT operation and local HT capabilities under `ac`. | They do not establish a production Block Ack exchange. | -| Protocol filter `/11ac/Ac_BlockAck\.test$` | The fixture passed its Action, BAR, and Block Ack frame-type assertions. The current BAR and Basic response selected 78 Mbps. | The generic assertions do not prove accepted parameters, complete bitmap semantics, or Compressed operation. | -| Module filter `Ieee80211VhtHtSupport_1\.test$` | The fixture passed all seven scenarios. Full and simplified management supply peer HT state. Scenario 1 removes that state at stop. | It disables Block Ack and therefore supplies capability and control evidence only. Stop also removes association; it is not the proposed fallback stimulus. | - -The exact runner commands follow. Each output redirection preserved the raw runner result in the review directory. - -```sh -inet_run_unit_tests -m debug -f 'Ieee80211(HtModeSet|VhtHtCapabilities)_1\.test$' --log-file /tmp/inet-compressed-blockack-review-20261013/unit.log > /tmp/inet-compressed-blockack-review-20261013/unit-output.txt 2>&1 -inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f '/11ac/Ac_BlockAck\.test$' --log-file /tmp/inet-compressed-blockack-review-20261013/protocol.log > /tmp/inet-compressed-blockack-review-20261013/protocol-output.txt 2>&1 -inet_run_module_tests -m debug -f 'Ieee80211VhtHtSupport_1\.test$' --log-file /tmp/inet-compressed-blockack-review-20261013/module.log > /tmp/inet-compressed-blockack-review-20261013/module-output.txt 2>&1 -``` - -The original record named generated stdout at `tests/protocol/wifi/work/Ac_BlockAck/test.out` and `tests/module/work/Ieee80211VhtHtSupport_1/test.out`. Those paths refer to the earlier checkout; this checkout contains neither output file. Later execution can replace generated output, and temporary-file cleanup can remove the raw review directory. The commands, source revision, selectors, and result limits above preserve the scope of this historical record. These results replace the obsolete absent-HT premise; they do not verify the proposed lifetime, lifecycle, or Compressed repairs. - -### Source comparison for evidence reuse - -The source comparison covers `6aefaef1f01a70134fa8283de708353d4a6d4fd8` and `c15860de93b3764c3e4ead69c6933fc5ae9ce957`. It found only these changed paths: - -- `WHATSNEW`. -- `doc/src/migration-guide/index.rst`. -- `doc/src/users-guide/ch-80211.rst`. - -The `src`, `tests`, and `bin` tree IDs match between those two historical revisions. Their runtime and test inputs therefore match the recorded behavioral source. This equality does not describe the current source after S1, which changes recipient source, tests, and fingerprints. The structural proof permits reuse only within the earlier checks' stated limits. It adds no Compressed, lifetime, lifecycle, or performance result. - -The comparison ran from the checkout root on 14 October 2026. Both commands returned exit status 0: - -```sh -git diff --name-only 6aefaef1f01a70134fa8283de708353d4a6d4fd8 c15860de93b3764c3e4ead69c6933fc5ae9ce957 -git diff --quiet 6aefaef1f01a70134fa8283de708353d4a6d4fd8 c15860de93b3764c3e4ead69c6933fc5ae9ce957 -- src tests bin .oppfeatures .oppfeaturestate Makefile .envrc -``` - -The first command reports the three documentation paths. The second confirms equality for the selected runtime and test inputs. The three directory tree IDs were also compared directly. No behavioral rerun was necessary solely for this documentation difference. - -The final review will check [the IEEE 802.11 domain rules](../../doc/project/domain/ieee80211.md), [minimal design](../../doc/project/rule/quality.md#qr-design-minimal), and [the plan procedure](../../doc/project/guide/write-an-implementation-plan.md). - -## Appendix B. Standards source notes +## 6. Verification and scope limits -Current-standard canonical clause identifiers use `ieee80211-2024:clause:` followed by the clause number. The IBSS table identifier is `ieee80211-2024:table:9-62`. The main evidence table gives physical PDF pages; printed pages differ by one in those ranges. +The [verification record](ieee80211-compressed-blockack-s4-execution.md) identifies the build, module, unit, protocol, and fingerprint results. Its commands and limits define the evidence scope. The fixture inventory is not itself proof that every fixture ran. -The evidence has these limits: +Basic single protection reserves only prepared successors. Prepared fragments can continue within the applicable exchange bounds. An unextracted successor starts another TXOP. Final zero-limit DATA reserves its immediate required response. Multiple protection retains its successor policy. These rules can increase contention and setup cost. -- IEEE Std 802.11e-2005 is present as `../inet-standards/80211e-2005.pdf`, relative to the repository root. The corpus processor does not register this document. No canonical corpus nodes or resolved reference records exist for it. Direct PDF inspection verifies normative Clauses 7.2.1.7, 7.2.1.8, and 9.10. Physical pages 52–54 and 118–122 correspond to printed pages 30–32 and 96–100. Figures 21b and 21e require visual inspection on physical pages 53 and 54. +The implementation does not supply a general complete-cost admission result for Basic DATA or complete NAV correctness. Current checks establish no general throughput, goodput, latency, or agreement-reuse cost result. MAC stop/start recovery also remains outside the lifecycle contract. - The source SHA-256 is `113625f95796112a44aec95378b79362de1c342ee951df5704521f11d1d227a1`. Basic retention makes no complete historical or current-standard conformance claim. +## Capability gaps -- Current control layouts use `ieee80211-2024:figure:9-47` and `ieee80211-2024:figure:9-53`, on physical pages 719 and 722. Their extracted nodes contain captions; PDF inspection supplies the bit layout. Figure 9-47's reference to Table 9-36 resolves. Tables 9-36 and 9-37 resolve at the canonical identifiers in Section 2. -- The individual-MPDU rule uses normative node `ieee80211-2024:clause:10.25.6.6.2.1`, physical pages 2065–2066. Its source locator is `ieee80211-2024@8602874:8608005`. PDF inspection confirms condition (a)(2)(v) and its continuation. This node has no extracted outgoing references. -- Full-state status uses normative node `ieee80211-2024:clause:10.25.6.3`, physical pages 2060–2061. Its source locator is `ieee80211-2024@8581659:8586175`. PDF inspection confirms its DATA and BAR update rules. This node has no extracted outgoing references. -- Clause 9.2.4.1.6 requires Retry for individual non-A-MPDU DATA. The prior PDF check on physical page 665 confirmed the exception's scope. The reference from Clause 10.23.2.12.1 to Clause 10.3.2.11 resolved. -- The extracted Clause 10.25.6.8 node ends at a figure. The PDF text on physical page 2073 supplies the positive-bit rule after that figure. That rule uses Block Ack start, the active transmit window, addresses, and TID. The shortened node is incomplete evidence. -- Relevant clause references resolved except the out-of-scope NGV reference from Clause 10.25.2 to Clause 31.2.3. This gap supplies no conformance claim for excluded features. +The checkout lacks production Compressed setup and exchanges, a Compressed default, and their integrated rate and timing evidence. Compressed-specific fragmentation restrictions, full-state receipt behavior, profile-conflict diagnostics, and end-to-end recovery therefore have no production support claim here. Existing helper coverage cannot replace those missing paths. diff --git a/plan/pending/ieee80211-compressed-blockack-s4-commits.md b/plan/pending/ieee80211-compressed-blockack-s4-commits.md new file mode 100644 index 00000000000..0fa051e151f --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-s4-commits.md @@ -0,0 +1,104 @@ +# S4 commit guide: complete Basic Block Ack transfers + +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [implementation status](ieee80211-compressed-blockack-replacement.md), [series plan](ieee80211-compressed-blockack-s4-corrections.md) + +The series separates preparation, the Retry correction, and transfer completion. Its final production source retains the reviewed Basic behavior. Protocol tests now observe the independent Retry correction and the complete short-transfer sequence. + +Agreement setup, finite DATA lifetime, filtered queue operations, and recipient teardown delivery come from earlier PRs. The [scope table](ieee80211-compressed-blockack-replacement.md#previous-prs-and-s4-scope) identifies them. + +## P0: Record the correction contract + +This documentation commit defines each commit's responsibility and verification. It adds explicit protocol test tasks before the two behavior changes. It changes no production behavior. + +## P1: Expose DATA preview without extraction + +Before this commit, the shared queue can identify a successor through extraction. Extraction changes ownership and can assign its sequence. BAR selection needs candidate information before those changes. + +After this commit, callers can borrow the next eligible frame and query its prospective sequence. Existing calls retain extraction as their default. The DATA-selection fixture checks queue ownership and the unchanged header. + +For example, queued DATA retains sequence 77 while preview reports prospective sequence 0. The frame stays in its queue. + +```text +Before: queue -> extract -> assign sequence -> inspect candidate +After: queue -> borrow candidate -> inspect prospective sequence + queue ownership and stored sequence remain unchanged +``` + +## P2: Mark DATA retries after failed Block Ack feedback + +Before this commit, the ACK handler already permits retransmission after missing or negative feedback. HCF can transmit that DATA with Retry clear. + +After this commit, HCF sets Retry before Tx receives the retransmitted DATA. Sequence and TID remain unchanged. This correction needs no preview, BAR arbitration, or completion change. + +The [protocol reviewer document](../../tests/protocol/wifi/blockack-data-retry.md) explains the lost-response and negative-bitmap cases. Both use production association and ADDBA. + +```mermaid +sequenceDiagram + participant O as Originator + participant R as Recipient + O->>R: Standalone DATA, Retry = 0 + O->>R: BAR + alt Run 0: feedback loss + R--xO: Block Ack lost at the radio tap + else Run 1: negative feedback + R-->>O: Block Ack with cleared bitmap + end + alt Before P2 + O->>R: Same sequence and TID, Retry = 0 + else After P2 + O->>R: Same sequence and TID, Retry = 1 + end +``` + +## P3: Add shared originator window operations + +Before this commit, the agreement stores its window start but lacks shared operations for the planned callers. After this commit, it also owns cyclic distance, bitmap membership, and advancement. + +No production caller uses those new operations until P4. This preparation changes no packet trajectory. The isolated unit fixture checks wraparound, unresolved positions, and the distinct bitmap boundary. + +For example, an accepted window at 4094 with size four includes 4094, 4095, 0, and 1. Its unresolved sequence 4095 prevents advancement to 0. + +```text +Before: agreement stores start; planned callers need window operations +After: caller supplies unresolved identities -> agreement advances window + caller supplies sequence -> agreement tests membership +``` + +## P4: Complete Basic BAR transfer resolution + +Before this commit, a short group can remain without final feedback. Channel access depends on DATA eligibility. Queue exhaustion or a full window can block completion below the BAR threshold. + +After this commit, HCF selects BAR before DATA extraction and permits BAR-only contention. The ACK policy separates peer/TID groups. Matched feedback resolves the group and permits shared window advancement. When no DATA remains, HCF starts completion DELBA. + +The sequence also limits Basic single protection to prepared successors. Unextracted DATA starts another TXOP. This policy can increase contention and repeated setup. Multiple protection retains its successor policy. + +For example, a one-position window and threshold five permit one outstanding DATA. BAR must resolve that position before another sequence can proceed. + +The [protocol reviewer document](../../tests/protocol/wifi/blockack-transfer-completion.md) explains both completion cases. Existing module fixtures retain expiry, collision, feedback, and timing controls. + +```mermaid +sequenceDiagram + participant O as Originator + participant R as Recipient + Note over O,R: Outstanding DATA below threshold five + Note over O: Queue empty or transmit window full + alt Before P4 + Note over O,R: No final feedback, transfer cannot complete + else After P4 + O->>R: BAR for the selected peer and TID + R-->>O: Matched Block Ack + Note over O: After all DATA resolves and none remains + O->>R: Completion DELBA + R-->>O: ACK + end +``` + +## P5: Describe current behavior and evidence + +This documentation commit distinguishes inherited prerequisites from S4 changes. It records each changed fingerprint row's cause and acceptance. It separates fresh execution from source equality and earlier evidence. + +## Evidence and limits + +The [verification record](ieee80211-compressed-blockack-s4-execution.md) supplies commands, source identities, and results. The protocol comparison records use identical test inputs on each behavior change and its parent. + +Basic remains the default compatibility profile. Compressed exchanges, A-MPDU, general Basic DATA admission, and complete NAV conformance remain outside this PR. diff --git a/plan/pending/ieee80211-compressed-blockack-s4-execution.md b/plan/pending/ieee80211-compressed-blockack-s4-execution.md new file mode 100644 index 00000000000..581cc6bfa53 --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-s4-execution.md @@ -0,0 +1,107 @@ +# Basic Block Ack transfer completion: current verification + +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [implementation status](ieee80211-compressed-blockack-replacement.md), [correction plan](ieee80211-compressed-blockack-s4-corrections.md) + +The final production source passes debug and release compilation, seven module files, three unit files, seven protocol files, and seven fingerprint cases. The verified `src` tree is `20aade533739a1877a87391909da7dd2a91c79c7`. The correction preserves that production tree and the accepted final fingerprint rows. It adds direct protocol evidence and separates the commit responsibilities. + +## Behavior and ownership + +HCF selects due Basic BAR work before DATA extraction and can contend without queued DATA. The ACK policy separates outstanding DATA by peer and TID within the accepted window. Below the threshold, it defers BAR only for the next eligible DATA in that group and window. + +For example, a window of one and a BAR threshold of five leave one DATA outstanding. The next sequence lies outside the window. HCF sends BAR. Positive feedback resolves that DATA and permits window progress. With no queued or numbered DATA at a safe exchange boundary, HCF queues completion DELBA. + +The originator agreement owns cyclic distance, bitmap membership, and window advancement. HCF supplies unresolved identities. BAR failure updates contention without a DATA retry charge. DATA retransmission sets Retry after failed feedback. Lifetime expiry can resolve outstanding positions and trigger completion teardown. + +Basic single protection reserves only prepared successors. Unextracted DATA starts another TXOP. Duration prediction borrows candidates without extraction. The [migration section](../../doc/src/migration-guide/development/ieee80211-basic-block-ack-extension-points.rst) defines the custom data-service and policy contracts. + +The [scope table](ieee80211-compressed-blockack-replacement.md#previous-prs-and-s4-scope) separates inherited lifecycle and recipient behavior from S4. These results describe the integrated source. They do not establish separate results for prerequisite PR heads. + +## Direct protocol evidence + +The [Retry comparison](../../doc/project/evidence/model/wifi/tests/blockack-data-retry-comparison.md) records lost-feedback and negative-bitmap cases. Both fail at the Retry predicate on the pinned base. Both pass with only HCF's Retry branch. The tests retain the captured sequence and TID across retransmission. + +The [completion comparison](../../doc/project/evidence/model/wifi/tests/blockack-transfer-completion-comparison.md) records queue exhaustion and a one-position window. Both fail before transfer completion. Both pass through BAR, matched feedback, completion DELBA, and ACK on the final source. Neither fixture supplies an accepted agreement. + +The wider protocol selection also checks duplicate ADDBA, dialog tokens, BAR without an agreement, inactivity, and same-AP reassociation. Two inherited fixtures need traffic controls because S4 completes agreements earlier. Their guides describe those controls. All seven files pass in `/tmp/pr1344-correction-final-protocol.log`. + +## Executed final checks + +| Check | Result | Scope | +| --- | --- | --- | +| Debug compilation | PASS | Final production source. | +| Release compilation | PASS | Final production source; no release runtime campaign. | +| Module tests | Seven files PASS | Completion cost, selection, scheduling, deferral, retirement, headers, and protection. | +| Unit tests | Three files PASS | Scheduling, shared window operations, and multiple protection. | +| Protocol tests | Seven files PASS | Thirteen seed-0 simulation runs; four runs exercise the new fixtures. | +| Fingerprints | Seven cases PASS | Existing non-graphical values; 274.663 seconds. | + +The final module log is `/tmp/pr1344-correction-final-modules.log`. The unit log is `/tmp/pr1344-opp-repl-final-contract-v2.log`. The fingerprint log is `/tmp/pr1344-correction-final-fingerprint.log`. Local `/tmp` artifacts can be absent in another checkout. + +These commands run from the checkout root: + +```sh +make MODE=debug -j12 +make MODE=release -j12 +inet_run_module_tests -m debug -f '(Ieee80211BlockAckCompletionCost_1|Ieee80211BlockAckScheduling_1|Ieee80211BlockAckDataSelection_1|Ieee80211BlockAckDeferral_1|Ieee80211BlockAckFragmentRetirement_1|Ieee80211HcfHeader_1|Ieee80211HcfMultipleProtection_2)\.test$' +inet_run_unit_tests -m debug -f '(Ieee80211OriginatorBlockAckWindow_1|Ieee80211BlockAckScheduling_1|Ieee80211MultipleProtection_1)\.test$' +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f 'WifiBlockAck(DataRetry|TransferCompletion|DuplicateAddba|DialogToken|BarWithoutAgreement|Inactivity|SameApReassociation)\.test$' +``` + +The fingerprint command runs from `tests/fingerprint`: + +```sh +./fingerprinttest -d -F tyf -t 3 -m 'showcases/wireless/blockack/|showcases/wireless/fragmentation/.*-c HCFfragblockack |showcases/wireless/txop/|examples/adhoc/qos/.*-c MacQos -r 1|examples/wireless/qos/.*-c MacQosWithBlockAck ' showcases.csv examples.csv +``` + +The CSV files supply each run, seed, limit, and computed-FCS setting. Graphical `tyf` values remain excluded. + +## Commit verification contract + +The [six-commit plan](ieee80211-compressed-blockack-s4-corrections.md) requires an incremental debug build at each output commit. The retained control worktree reuses compatible objects. The `opp_repl` selection uses the checkout's explicit `inet_project` instance. + +| Commit | Direct selection | +| --- | --- | +| P0: correction contract | Existing multiple-protection unit fixture. | +| P1: preview | DATA-selection module fixture. | +| P2: Retry | Retry protocol fixture; seven fingerprints with five approved intermediate values. | +| P3: shared arithmetic | New window unit fixture and unchanged Retry protocol fixture. | +| P4: completion | Both new protocol fixtures, three unit files, and seven module files. | +| P5: current documents | Both new protocol fixtures and the final related selection. | + +The execution log under `ai-logs/executions/2026-10-22_pr1344-corrections.md` records exact safe points and results. That directory is local workflow state. It does not form portable test evidence by itself. + +## Fingerprint causes and acceptance + +The final source retains the seven accepted non-graphical baseline rows. The original S4 execution record states that the user approved these values on 2026-10-15. Its immutable source is commit `18bba0a41ab8c00aefaeca7de233fd68cbc4ee7f`, before the current documentation replacement. This correction restores that acceptance record. It grants no approval for a new intermediate value. + +| Configuration and run | Final fingerprints | Cause | +| --- | --- | --- | +| Block Ack `NoFragmentation`, run 0 | `9161-625d/tplx;6a02-09aa/~tNl;cbc0-3b0a/~tND` | Final Normal Ack DATA reserves its immediate ACK. Setup and BAR order also change. | +| Block Ack `Fragmentation`, run 0 | `36f4-5c22/tplx;66b0-94cd/~tNl;45f3-af4f/~tND` | Final Block Ack DATA reserves zero. BAR obtains a separate TXOP. | +| Block Ack `MixedTraffic`, run 0 | `351b-d4ba/tplx;5c1f-69a8/~tNl;6182-6da3/~tND` | The current reservation excludes an unextracted large successor. Agreement setup can precede that DATA. | +| Fragmentation `HCFfragblockack`, run 0 | `2597-1e2e/tplx;d269-c6ae/~tNl;2f5c-4cca/~tND` | Retransmitted DATA sets Retry after failed feedback. The final burst boundary also changes. | +| TXOP `General`, run 0 | `ab91-87f3/tplx;f30c-6aa4/~tNl` | HCF excludes an unextracted successor from the reservation. The frame sequence ends at that same boundary. | +| Ad hoc `MacQos`, run 1 | `4fd2-bb56/tplx;ecaf-89f7/~tNl;9398-0fae/~tND` | Final Normal Ack DATA reserves SIFS plus ACK. Further extraction requires another TXOP. | +| Wireless `MacQosWithBlockAck`, run 0 | `6edb-0326/tplx;2a8a-50ee/~tNl;4301-882d/~tND` | ADDBA reserves its immediate ACK without unrelated queued DATA. Later extraction can change aggregation. | + +The causal behavior commit must name these rows and their causes. A matching fingerprint checks the recorded trajectory. Direct protocol and module assertions establish the specific correctness claims. + +## Intermediate fingerprints + +The user approved five intermediate `~tND` values on 2026-10-22. They belong to the isolated Retry commit. All seven event and packet-length fingerprints remain equal to their parent values. TXOP General and ad hoc MacQos run 1 retain every selected value. + +| Configuration | Parent `~tND` | Retry-only `~tND` | +| --- | --- | --- | +| Block Ack NoFragmentation | `2cd4-4795` | `0f38-6042` | +| Block Ack Fragmentation | `feff-7e88` | `ddec-9e3d` | +| Block Ack MixedTraffic | `9724-d1ce` | `2839-8753` | +| Fragmentation HCFfragblockack | `c474-7174` | `bbe5-0771` | +| Wireless MacQosWithBlockAck | `538e-67df` | `1914-cd4d` | + +Each row uses run 0. Retransmitted Block Ack DATA changes its Retry header and computed FCS. The later completion commit replaces these intermediate values with the accepted final rows above. The final CSV files remain byte-identical to the original PR head. + +## Limits + +Basic remains the operational profile and default. HCF rejects Compressed selection during initialization. General Basic DATA admission, complete NAV conformance, A-MPDU, delayed Block Ack, and Multi-TID Block Ack remain outside this claim. + +A fixed seed establishes these deterministic exchange paths. It does not establish probabilistic robustness or performance. Build and mechanical gate passes do not establish protocol correctness. Local verification supplies no CI result for a new commit. The correction self-check does not replace an independent review. diff --git a/plan/pending/ieee80211-edca-data-lifetime.md b/plan/pending/ieee80211-edca-data-lifetime.md index 45f8db4540e..caf97ef4cb8 100644 --- a/plan/pending/ieee80211-edca-data-lifetime.md +++ b/plan/pending/ieee80211-edca-data-lifetime.md @@ -1,111 +1,57 @@ -# Implementation plan: enforce EDCA DATA lifetime +# EDCA DATA lifetime contract -Original source observation revision: `feat/ieee80211-txop-single-protection` at `b06e8768b0e9c78b7a3665b9c69b0e0da9f30e41`. -Current integration source: local `master` at `633b071e213b825ce6019ff3e344ae49f52791ff`, with S1, R1, and S3a merged. -Plan revision: 2026-10-15, revision 11. -Source basis: IEEE Std 802.11-2024 and the revisions above. Source observations do not establish runtime results. +> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [lifetime handler](../../src/inet/linklayer/ieee80211/mac/lifetime/EdcaTransmitLifetimeHandler.cc), [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) -Approval: the maintainer's 14 October 2026 instruction, "proceed", after the revision-24 review authorizes the earlier document corrections. The maintainer's later explicit decision accepts the proposed 0.512-second DATA lifetime default. This decision selects the default and authorizes its record in the plan. These instructions authorize no implementation, recorded expectation change, or publication. +HCF enforces a finite deadline for every QoS DATA frame that it accepts. This contract applies with Block Ack enabled or disabled. DCF lifetime, complete management lifetime, and MAC stop/start recovery remain outside the contract. -The [completed S1 record](ieee80211-compressed-blockack-replacement.md#completed-s1-and-recorded-verification) supplies Basic recipient capacity and ordered delivery. PR #1320 is merged in the integration source. Its original feature-head evidence establishes no fresh runtime result for main or S2 expiration. Lifetime observations retain `b06e8768` until the separate S2 review refreshes them against its selected parent. +## Previous PRs and S4 scope -Revision 11 updates the integration pin for merged R1 and S3a. Their completion does not establish S2 completion. The [umbrella source record](ieee80211-compressed-blockack-replacement.md#source-base-and-publication-boundaries) distinguishes the integration source from this checkout's separate S2 implementation and local edits. This review adds no runtime result or implementation authority. +Merged [PR #1331](https://github.com/inet-framework/inet/pull/1331) supplies the lifetime mechanism described here. It includes identity-based deadlines, the 0.512-second default, expiry, fragment and aggregate rules, and safe completion across expiry. Merged [PR #1343](https://github.com/inet-framework/inet/pull/1343) uses that deadline during agreement setup and teardown recovery. These mechanisms are already present in S4's master base. -## 1. Core intended behavior +This S4 branch, [PR #1344](https://github.com/inet-framework/inet/pull/1344), connects lifetime disposal to transmit-window advancement and final-transfer completion. HCF removes resolved positions without sequence reuse. If the final unresolved DATA expires and no DATA remains, HCF can start completion DELBA at a safe exchange boundary. -HCF, the hybrid coordination function, will enforce a finite lifetime for all QoS DATA. This lifetime applies with either value of `isBlockAckSupported`. EDCA means enhanced distributed channel access. An EDCAF is an EDCA function that serves one access category. +The recipient's complete-unit release on ordinary teardown comes from PR #1343. S4 supplies the final-expiry trigger that reaches that existing path. S4 does not change the lifetime default, restart retry deadlines, or introduce another lifetime store. The [main scope table](ieee80211-compressed-blockack-replacement.md#previous-prs-and-s4-scope) gives the wider implementation boundary. -An MSDU is the data unit that the MAC receives from its service user. An A-MSDU combines several MSDUs inside one MPDU. An MPDU is a MAC frame with its header and frame check sequence. +## 1. Core behavior -This prerequisite supplies the disposal bound for [Block Ack agreement recovery](ieee80211-blockack-agreement-lifecycle.md) and [Compressed Block Ack retransmission](ieee80211-compressed-blockack-replacement.md). It also applies to Normal Ack DATA without an agreement. A Block Ack capability switch will not select whether DATA has a lifetime. +The per-EDCAF parameter `msduLifetime` defaults to 0.512 seconds. Queue residence, setup deferral, retries, and ACK-policy changes consume that deadline. The parameter requires a positive, finite value that simulation time can represent. Zero, negative, and nonfinite values fail initialization. There is no unlimited mode. -Hypothetical example: HCF accepts QoS DATA at 0 s with the default 0.512-second lifetime. Contention prevents transmission before 0.7 s. HCF discards that DATA at its deadline, with Block Ack support either enabled or disabled. DATA that Tx already accepted follows the safe-completion rule below. - -DCF lifetime and complete management-frame lifetime remain outside this change. The current MAC stop and crash handlers do not reset all child state. This plan makes no stop/start recovery claim. +Hypothetical example: HCF accepts DATA at zero seconds and channel access remains blocked until 0.7 seconds. The default deadline expires at 0.512 seconds. A one-second lifetime permits an attempt after channel access resumes, subject to other eligibility and disposal rules. ## 2. Current source and standard support -[EdcaTransmitLifetimeHandler](../../src/inet/linklayer/ieee80211/mac/lifetime/EdcaTransmitLifetimeHandler.cc) stores start times by sequence number. Two peers can therefore share one key. Its current start point excludes queue residence. EDCA does not construct or call this handler in the inspected source. - -[Edcaf.ned](../../src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.ned) has no `msduLifetime` parameter. HCF and the QoS data service own the entry and replacement paths that must supply lifetime identity. - -IEEE Std 802.11-2024 Clause 10.23.2.12.1 requires the relevant QoS transmit timers. Its scope does not depend on Block Ack support. Clause 10.25.6.7.1 supplies the related Block Ack retransmission conditions. Their normative corpus identifiers are `ieee80211-2024:clause:10.23.2.12.1` and `ieee80211-2024:clause:10.25.6.7.1`. Physical PDF pages are 2016–2017 and 2068–2070. - -Normative Annex C.3 defines `dot11EDCATableMSDULifetime` for non-AP stations and `dot11QAPEDCATableMSDULifetime` for APs. Both attributes use TU and default to 500. One TU is 1024 microseconds, so 500 TU equals 0.512 seconds. Their physical PDF pages are 5418 and 5420. The canonical node is `ieee80211-2024:clause:C.3`; source offsets are 22781782 and 22790375. Corpus text supplied both values without PDF inspection; the retrieved Clause 10.23.2.12.1 reference to Clause 10.3.2.11 resolved. - -The implementation must reproduce missing queue expiration through production HCF before it changes that path. Source inspection establishes the absent integration; it does not establish a measured loss or delay. - -## 3. Proposed ownership and behavior - -The current `EdcaTransmitLifetimeHandler` will own deadlines for QoS DATA regardless of HCF Block Ack support. EDCA will construct this owner from a positive, finite `msduLifetime` parameter on each EDCAF. The accepted default is 0.512 seconds, which matches the standard's 500-TU defaults for AP and non-AP stations. The compatibility record below identifies its acceptance and effect. HCF will register DATA when `processUpperFrame` accepts it, before queue insertion or sequence assignment. The deadline therefore includes queue residence and setup deferral. - -Management exchanges retain their existing retry procedure. This change does not claim complete management-lifetime support. - -The parameter uses seconds and permits other positive, finite values within the supported simulation-time range. Initialization will reject zero, negative, and nonfinite values. Each configured value supplies a DATA deadline; it does not guarantee prompt recovery or expiry within the simulation duration. A large value can postpone lifetime disposal beyond the end of a run. This plan provides no unlimited mode because unresolved DATA must retain a deadline when no earlier disposal or successful exchange resolves it. - -For compatibility studies, users can select a finite lifetime longer than their simulation duration. Hypothetical example: `**.mac.hcf.edca.edcaf[*].msduLifetime = 100s` prevents lifetime expiry for newly accepted DATA during a 10-second run. This proposed setting does not promise unchanged fingerprints or complete legacy behavior. Response acceptance, teardown, queue loss, and terminal failure can still resolve or discard DATA earlier. A recovery path that relies on lifetime expiry inherits the configured delay. - -### Lifetime compatibility decision - -The standards basis establishes the timer's scope and the 500-TU default. It does not measure the effect on current INET simulations. New disposal can occur with Block Ack disabled when retained DATA reaches its deadline. Congestion alone does not prove that a retained frame reaches that age. - -On 14 October 2026, the maintainer explicitly accepted the proposed 0.512-second default. The value matches the 500-TU standards basis and supplies a finite disposal deadline. This acceptance includes the proposed scope with Block Ack disabled. It permits new disposal when retained QoS DATA reaches its deadline. The compatibility decision before L1 is satisfied. Source review, production reproduction, and measured expiry remain required by S2. - -An opt-in or unlimited compatibility mode would require a separate design. That design must state its standards limits and supply another finite disposal bound wherever Block Ack recovery requires progress. This plan retains a finite lifetime and the long-finite compatibility setting above. - -### Deadline ownership and safe completion - -The handler will key retained deadlines by local packet identity, not sequence number. The QoS data service will transfer this identity information when it replaces source frames with an A-MSDU or fragments. Fragments share their source MSDU deadline. Each MSDU addition restarts the aggregate timer under Clause 10.23.2.12.1. Retransmission and ACK-policy selection will not restart a deadline. Queue discard, acknowledgment, teardown, and peer cleanup will remove the corresponding lifetime entries. - -The [current aggregate builder](../../src/inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.cc) adds its inputs synchronously at one simulation time. Its aggregate deadline therefore uses that construction time and the applicable AC lifetime. - -Hypothetical example: an MSDU arrives at 0 seconds and joins an A-MSDU at 0.4 seconds under Normal Ack. This example has no active Block Ack agreement and no pending setup or teardown for that peer and TID. With a 0.512-second lifetime, the aggregate deadline is 0.912 seconds. The original 0.512-second deadline must not discard that aggregate. Queue expiry must remove already expired inputs before aggregation; aggregation cannot revive them. - -This repair will add narrow operations to the concrete EDCA lifetime owner. It needs no change to DCF's lifetime interface. HCF will create one timer for the earliest DATA deadline across its EDCA queues and release it at destruction. Queue owners will remove expired DATA and its ACK state before further selection. HCF will also check the deadline before submission to Tx, including any proposed IFS delay. No DATA transmission may start after that deadline. +[Edca](../../src/inet/linklayer/ieee80211/mac/channelaccess/Edca.cc) owns the lifetime handler. [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) registers QoS DATA before queue insertion and uses one timer for the earliest deadline. The queue owner removes expired frames. HCF clears their acknowledgment and recovery state. -An accepted transmission can complete after its deadline; HCF will retain the referenced frame until its current exchange completes. Expiration will prohibit another attempt and trigger disposal at that safe boundary. HCF will not delete a frame that Tx or the frame sequence still references. The lifetime owner will exclude that expired retained entry from its next timer selection. A successful response to the accepted attempt can still acknowledge it. +IEEE Std 802.11-2024 Clause 10.23.2.12.1 covers QoS transmit lifetime. Clause 10.25.6.7.1 supplies related Block Ack retransmission conditions. Annex C.3 supplies 500 TU defaults through `dot11EDCATableMSDULifetime` and `dot11QAPEDCATableMSDULifetime`. The conversion is 500 TU = 0.512 seconds. These references define the requirement basis; this document adds no standards retrieval result. -Hypothetical example: every response reports DATA 100 as absent but acknowledges later DATA. DATA 100 remains eligible until its original deadline, even while successful Block Ack responses refresh agreement inactivity. At expiration, HCF discards DATA 100 and advances the transmit window from resolved status. Later DATA or BAR can advance the recipient past that gap. No inactivity refresh can extend DATA 100's lifetime. +## 3. Implemented ownership and behavior -## 4. Separate implementation stages +The handler keys deadlines by local frame identity. Two peers can use the same sequence number without a shared deadline. The QoS data service clears borrowed source references before source deletion. Fragments retain their source deadline. Retransmission and ACK-policy changes do not restart it. -| Stage and purpose | Responsible component and change | Expected result and direct verification | -| --- | --- | --- | -| L1. Establish lifetime identity. | The lifetime handler stores local packet identities. The QoS data service transfers deadlines across frame replacement. Add the EDCAF parameter with unit seconds and default 0.512 seconds. | Two peers with identical sequence numbers retain separate deadlines. Fragments share the source deadline. A-MSDU construction follows the specified lifetime rule. Invalid lifetime values fail at initialization. | -| L2. Enforce expiration through HCF. | HCF registers DATA before queue insertion. Queue owners remove expired DATA. HCF prevents another attempt after expiration and preserves accepted exchanges until completion. | Production tests expire queued Normal Ack DATA with both Block Ack switch values. Expiration during an accepted exchange causes no premature deletion or later retry. | -| L3. Integrate consumers and document compatibility. | Agreement recovery and Compressed retransmission consume the same deadlines after their respective stages exist. Update user documentation and migration guidance. | Deferred setup and repeated Block Ack failures cannot extend a deadline. Ordinary EDCA users can select a suitable lifetime without enabling Block Ack. | +A new A-MSDU starts a deadline at aggregate construction. The current builder adds all sources at one simulation time. Only tracked sources participate in lifetime replacement, including when `qOsCheck=false` permits non-QoS or mixed sources. A new QoS aggregate receives its own deadline even when no source was tracked. Expired inputs cannot enter an aggregate. -L1 and L2 form the publishable lifetime change in umbrella stage S2. Their commits must build and expose no unused public contract. L3 integration tests accompany the consumers that introduce those paths; they do not block standalone Normal Ack verification. +Hypothetical example: an MSDU accepted at zero seconds joins an A-MSDU at 0.4 seconds without an active or pending agreement. The new aggregate expires at 0.912 seconds. Its source deadline was 0.512 seconds. An expired input cannot receive this extension. Uncertain setup prevents aggregation that could restart the deferred batch's deadlines. -The lifetime compatibility decision for L1 is accepted at 0.512 seconds. Before S2 starts, refresh the older source observations and caller assessment against its pinned parent. The [umbrella decision table](ieee80211-compressed-blockack-replacement.md#maintainer-decisions) records that acceptance and the independent stage boundaries. +### Compatibility setting -Finite lifetime changes Block Ack-disabled EDCA simulations as well as Block Ack-enabled simulations. Affected callers include the single-protection path, the QoS and aggregation showcases, and the `MacQos` examples. Queue residence can now cause disposal before any transmission. User documentation must state the default, parameter location, and the compatibility setting and limits above. It must distinguish aggregation's timer restart from retransmission and ACK-policy changes, which do not restart the deadline. No number of changed fingerprints is assumed before measurement. +For a ten-second study, `**.mac.hcf.edca.edcaf[*].msduLifetime = 100s` prevents lifetime expiry of newly accepted DATA during that interval. Acknowledgment, teardown, queue loss, and terminal failure can still remove DATA. This setting guarantees neither identical fingerprints nor complete compatibility. Recovery that depends on expiry inherits the longer delay. -## 5. Verification and evidence +### Deadline and safe completion -Add a proposed `tests/module/Ieee80211EdcaDataLifetime_1.test` for production expiration and safe completion. Add focused helper assertions for identity transfer only where the production fixture cannot expose the necessary state. +HCF checks the deadline before submission to Tx, with the proposed IFS. No new DATA attempt can start at or after the deadline. An accepted attempt can finish across it and receive successful acknowledgment. -Use these direct cases: +HCF retains referenced frames until exchange cleanup releases the context. It prevents another attempt and excludes expired retained entries from timer selection. This permits safe completion without repeated zero-time expiration events. -- Check the 0.512-second default for AP and non-AP EDCA. Reject zero, negative, and nonfinite values. Confirm that another positive, finite value controls expiration. -- Accept DATA before queue insertion. Hold channel access beyond its deadline. Check disposal without transmission for both Block Ack switch values. -- Give two peers the same TID and sequence number. Expire one frame. Check that the other frame retains its own deadline. -- Aggregate DATA. Fragment separate DATA. Check the resulting deadlines against Clause 10.23.2.12.1. -- A-MSDU extension: Use a peer and TID with no active Block Ack agreement or pending setup or teardown. Accept an MSDU at 0 seconds with a 0.512-second lifetime. Aggregate it under Normal Ack at 0.4 seconds. Hold the aggregate without transmission. Check retention past 0.512 seconds and expiry at 0.912 seconds. -- Expired aggregation input: Expire an input before aggregation. Check that the expired input cannot enter the aggregate. -- Long finite lifetime: Select a 100-second lifetime for a 10-second run. Check that newly accepted DATA does not expire through lifetime during that run. Check earlier acknowledgment and disposal paths separately. Do not treat a matching delivery result as proof of unchanged fingerprints. -- Expire DATA before a proposed IFS ends. Check that HCF rejects submission before transmission can start after expiration. -- Expire DATA during an accepted exchange. Permit its completion. Check that HCF retains references safely and prohibits another attempt. -- Dispose of queued DATA. Acknowledge other DATA. Reset a peer. Check removal of lifetime entries without changes to another peer. +## 4. Agreement integration -The [replacement plan's verification scope](ieee80211-compressed-blockack-replacement.md#6-verification-and-scope-limits) supplies the common regression selection and the current S1 recipient fixture. Retain all eight Basic recipient scenarios as regression checks. Include the Block Ack-disabled EDCA rows because this prerequisite changes their supported behavior. Compare each stage with its pinned parent before any baseline proposal. S1's recorded 12-configuration result does not verify S2's proposed 19-row selection or its measured expiration requirement. +Setup and DELBA recovery consume the existing DATA deadline. HCF advances the transmit window when lifetime disposal resolves an outstanding position. It uses the same identity owner rather than a separate agreement lifetime store. -Measure delivered DATA, lifetime disposal, queue delay, and completion delay under controlled congestion. Keep traffic, seed, rates, and channel conditions identical across the comparison. Record the lifetime value and Block Ack switch for each run. A short run without expiration cannot prove this behavior. +Final expiry can trigger completion DELBA even when the recipient still holds acknowledged complete units behind a gap. Ordinary recipient teardown delivers those complete units before resource release. Association reset discards old buffered units instead. The [lifecycle contract](ieee80211-blockack-agreement-lifecycle.md#recipient-buffer-disposition-at-teardown) defines these separate outcomes. -S2 requires at least one Block Ack-disabled congestion case that demonstrates the default lifetime boundary. Select a common regression row only if measured frame ages establish that condition. Otherwise, add a controlled congestion variant with its parameters and parent comparison recorded separately. Record each affected frame's MAC acceptance time, disposal time, age, and disposal cause. Separate lifetime disposal from queue-capacity loss and retry-limit loss. +Hypothetical example: repeated Block Ack feedback leaves DATA 100 unresolved. Its original deadline expires despite agreement activity. HCF clears its transmit state and permits window progress without sequence reuse. With no further DATA, completion teardown can release complete recipient units behind that gap. -The [QoS showcase](../../showcases/wireless/qos/omnetpp.ini) offers approximately 57.7 Mbps of application payload against a configured 54 Mbps radio rate. Its [pending queues](../../src/inet/linklayer/ieee80211/mac/queue/PendingQueue.ned) default to 100 frames. That offered load does not prove that a retained frame reaches 0.512 seconds of age. The [MacQos example](../../examples/wireless/qos/omnetpp.ini) selects unlimited pending queues, but this setting alone does not prove sufficient delay. Neither configuration supplies measured expiry evidence in this plan. +## 5. Verification and limits -Compare the parent without expiration, the 0.512-second default, and an explicit one-second setting under the same congestion. Hypothetical example: unaggregated DATA waits 0.7 seconds before channel access. The default discards it; the one-second setting still permits an attempt. This comparison measures the compatibility cost without an unlimited production mode. +The source includes lifetime, identity, aggregation, arbitration, and congestion fixtures under `tests/module`. The [current S4 verification record](ieee80211-compressed-blockack-s4-execution.md) defines the executed selection. It includes lifetime interactions through Block Ack fixtures but does not report a new standalone lifetime campaign. -Follow [run-the-gates.md](../../doc/project/guide/run-the-gates.md) for the required build and focused test commands. Record commands, modes, selected cases, and results when implementation exists. This revision adds no execution result. The separate S1 record owns completed recipient verification; lifetime and performance results remain unverified. +The source contract applies independently of Block Ack support. Current S4 results do not establish complete Compressed recovery, general performance, or MAC stop/start behavior. Lifetime disposal, inactivity teardown, queue overflow, and retry-limit disposal remain distinct causes. From dcd2f66a18fc98cbd062d30ef9898d3380aebc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:48:05 +0200 Subject: [PATCH 07/16] physicallayer: add: retain copied physical timing evidence Before this commit, interval consumers need retained transmission pointers or new propagation calculations. Cache expiry removes the relevant objects, so later MAC decisions cannot safely use those pointers. After this commit, radio media supply copied transmission endpoints and cached arrival endpoints. A snapshot survives cache expiry without object pointers. The receiver includes the transmission identifier in its local signal-time indication. Later delivery can use that identity to locate the evidence. For example, a recipient can retain arrival endpoints after the medium removes the reception. Const cache queries return absent results without insertion. Missing or identical transmission identities supply no interval evidence. The direct fixture checks copied values and absent states. The shared mechanism supplies no protocol-specific interval decision. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.physicallayer.wireless.common | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- ...sicallayer-radio-medium-timing-queries.rst | 6 + ...sicallayer-radio-medium-timing-queries.txt | 3 + .../common/base/packetlevel/ReceiverBase.cc | 1 + .../MapCommunicationCache.cc | 10 ++ .../MapCommunicationCache.h | 1 + .../ReferenceCommunicationCache.cc | 10 ++ .../ReferenceCommunicationCache.h | 1 + .../VectorCommunicationCache.cc | 14 +- .../VectorCommunicationCache.h | 1 + .../packetlevel/ICommunicationCache.h | 2 + .../contract/packetlevel/IRadioMedium.h | 21 +++ .../common/contract/packetlevel/SignalTag.msg | 1 + .../packetlevel/TransmissionTimingSnapshot.h | 31 ++++ .../wireless/common/medium/RadioMedium.cc | 34 +++++ .../wireless/common/medium/RadioMedium.h | 3 + tests/unit/TransmissionTimingSnapshot_1.test | 138 ++++++++++++++++++ 16 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 doc/src/migration-guide/development/physicallayer-radio-medium-timing-queries.rst create mode 100644 doc/whatsnew/incompatible/physicallayer-radio-medium-timing-queries.txt create mode 100644 src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h create mode 100644 tests/unit/TransmissionTimingSnapshot_1.test diff --git a/doc/src/migration-guide/development/physicallayer-radio-medium-timing-queries.rst b/doc/src/migration-guide/development/physicallayer-radio-medium-timing-queries.rst new file mode 100644 index 00000000000..c5e51ce4c1a --- /dev/null +++ b/doc/src/migration-guide/development/physicallayer-radio-medium-timing-queries.rst @@ -0,0 +1,6 @@ +Radio medium timing queries +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Custom radio media must implement ``getTransmissionTimingSnapshot()`` and ``computePrecedingSignalGap()``. These queries supply copied physical endpoints from actual transmissions and cached arrivals. Unavailable endpoints produce an absent result. The snapshot must not retain transmission, arrival, or radio pointers. Communication caches must implement the const ``findCachedArrival()`` query without insertion or other mutation. + +``SignalTimeInd`` now carries the medium's transmission identifier. The receiver supplies this local indication when it accepts a transmission. Consumers can associate later frame delivery with the corresponding physical timing evidence. The identifier is local metadata; it adds no transmitted field. diff --git a/doc/whatsnew/incompatible/physicallayer-radio-medium-timing-queries.txt b/doc/whatsnew/incompatible/physicallayer-radio-medium-timing-queries.txt new file mode 100644 index 00000000000..97fae85a9bc --- /dev/null +++ b/doc/whatsnew/incompatible/physicallayer-radio-medium-timing-queries.txt @@ -0,0 +1,3 @@ +Radio medium timing queries + +Radio media now expose copied transmission endpoints and cached arrival endpoints. Consumers can preserve physical timing evidence after cache expiry without retained transmission pointers or new propagation calculations. Receivers attach the transmission identifier to SignalTimeInd so later frame delivery can identify that evidence. Custom radio media and communication caches must implement the new query methods described in the migration guide. diff --git a/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc b/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc index a3c828e2457..e1aceee6450 100644 --- a/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc +++ b/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc @@ -102,6 +102,7 @@ const IReceptionResult *ReceiverBase::computeReceptionResult(const IListening *l auto signalTimeInd = packet->addTagIfAbsent(); signalTimeInd->setStartTime(reception->getStartTime()); signalTimeInd->setEndTime(reception->getEndTime()); + signalTimeInd->setTransmissionId(reception->getTransmission()->getId()); if (snir->getMax() == 0) { auto errorRateInd = packet->addTagIfAbsent(); errorRateInd->setSymbolErrorRate(1); diff --git a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc index b5632d21994..c5032d10a4d 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc @@ -14,6 +14,16 @@ namespace physicallayer { Define_Module(MapCommunicationCache); +const IArrival *MapCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + auto entry = transmissionCache.find(transmission->getId()); + if (entry == transmissionCache.end() || !entry->second.receptionCacheEntries) + return nullptr; + const auto& receptions = *entry->second.receptionCacheEntries; + auto reception = receptions.find(receiver->getId()); + return reception == receptions.end() ? nullptr : reception->second.arrival; +} + MapCommunicationCache::~MapCommunicationCache() { for (auto& it : transmissionCache) diff --git a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h index e9d9c8fa0e5..8ea9821446c 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h @@ -47,6 +47,7 @@ class INET_API MapCommunicationCache : public CommunicationCacheBase public: virtual ~MapCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "MapCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc index b735ca369e3..2fa4300abc4 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc @@ -14,6 +14,16 @@ namespace physicallayer { Define_Module(ReferenceCommunicationCache); +const IArrival *ReferenceCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + int transmissionIndex = transmission->getId(); + int radioIndex = receiver->getId(); + if (transmissionIndex < 0 || transmissionIndex >= int(transmissionCache.size()) || radioIndex < 0) + return nullptr; + auto receptions = transmissionCache[transmissionIndex].receptionCacheEntries; + return receptions && radioIndex < int(receptions->size()) ? receptions->at(radioIndex).arrival : nullptr; +} + ReferenceCommunicationCache::~ReferenceCommunicationCache() { for (auto& transmissionCacheEntry : transmissionCache) diff --git a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h index 8c27a6322ad..b4de8fa4887 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h @@ -42,6 +42,7 @@ class INET_API ReferenceCommunicationCache : public CommunicationCacheBase public: virtual ~ReferenceCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "ReferenceCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc index 3351dc55b87..5e2f96da7b2 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc @@ -14,6 +14,17 @@ namespace physicallayer { Define_Module(VectorCommunicationCache); +const IArrival *VectorCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + int transmissionIndex = transmission->getId() - baseTransmissionId; + int radioIndex = receiver->getId() - baseRadioId; + if (baseTransmissionId < 0 || baseRadioId < 0 || transmissionIndex < 0 || + transmissionIndex >= int(transmissionCache.size()) || radioIndex < 0) + return nullptr; + auto receptions = transmissionCache[transmissionIndex].receptionCacheEntries; + return receptions && radioIndex < int(receptions->size()) ? receptions->at(radioIndex).arrival : nullptr; +} + VectorCommunicationCache::~VectorCommunicationCache() { for (auto& transmissionCacheEntry : transmissionCache) @@ -141,7 +152,8 @@ void VectorCommunicationCache::removeTransmission(const ITransmission *transmiss const ITransmission *VectorCommunicationCache::getTransmission(int id) const { - ASSERT(baseTransmissionId != -1); + if (baseTransmissionId == -1) + return nullptr; int transmissionIndex = id - baseTransmissionId; if (0 <= transmissionIndex && transmissionIndex < (int)transmissionCache.size()) return transmissionCache[transmissionIndex].transmission; diff --git a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h index 3b40aee60bf..3f8e0dbeea0 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h @@ -60,6 +60,7 @@ class INET_API VectorCommunicationCache : public CommunicationCacheBase public: virtual ~VectorCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "VectorCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h index 8688c8d06e0..55f2385206c 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h @@ -65,6 +65,8 @@ class INET_API ICommunicationCache : public virtual IPrintableObject /** @name Reception Cache */ //@{ virtual const IArrival *getCachedArrival(const IRadio *receiver, const ITransmission *transmission) = 0; + // This lookup returns nullptr for absent state. It allocates no cache entry. + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const = 0; virtual void setCachedArrival(const IRadio *receiver, const ITransmission *transmission, const IArrival *arrival) = 0; virtual void removeCachedArrival(const IRadio *receiver, const ITransmission *transmission) = 0; diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h index a56ffb5d8af..0a70986456a 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h @@ -8,6 +8,8 @@ #ifndef __INET_IRADIOMEDIUM_H #define __INET_IRADIOMEDIUM_H +#include + #include "inet/physicallayer/wireless/common/contract/packetlevel/IWirelessSignal.h" #include "inet/common/packet/Packet.h" #include "inet/environment/contract/IMaterial.h" @@ -25,6 +27,7 @@ #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IReceptionDecision.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h" namespace inet { namespace physicallayer { @@ -171,6 +174,24 @@ class INET_API IRadioMedium : public virtual IPrintableObject */ virtual const ITransmission *getTransmission(int id) const = 0; + /** + * Copies the physical endpoints of a live transmission without new propagation calculations. + * An absent transmission returns a snapshot with transmissionId equal to -1. + * An absent radio entry means that the medium has no cached arrival for that radio. + * The caller owns the returned value. Later cache removal does not change it. + */ + virtual TransmissionTimingSnapshot getTransmissionTimingSnapshot(int id) const = 0; + + /** + * Returns the interval before the current transmission at its transmitter. + * For the same transmitter, the interval starts at the previous transmission end. + * For different transmitters, it starts at the previous signal arrival end at the current transmitter. + * Missing current transmission or arrival data returns no value. + * The method uses the supplied snapshot and changes no cache or propagation state. + */ + virtual std::optional computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const = 0; + /** * Returns the listening decision that describes what the receiver detects * on the medium. diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg b/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg index abca21a254b..cf9802507b8 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg @@ -126,4 +126,5 @@ class SignalTimeInd extends SignalTagBase { simtime_t startTime; simtime_t endTime; + int transmissionId = -1; // Medium transmission identity; -1 means unavailable. } diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h new file mode 100644 index 00000000000..312e9bc7476 --- /dev/null +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_TRANSMISSIONTIMINGSNAPSHOT_H +#define __INET_TRANSMISSIONTIMINGSNAPSHOT_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +/** + * This value copies the physical endpoints of one transmission from the medium cache. + * The endpoints retain the propagation result that the medium used for each radio. + * No packet, transmission, arrival, or radio pointer remains in this value. + * A transmissionId of -1 means that the medium no longer has the transmission. + */ +struct INET_API TransmissionTimingSnapshot +{ + int transmissionId = -1; + int transmitterRadioId = -1; + simtime_t startTime = -1; // First transmitted bit, in seconds. + simtime_t endTime = -1; // Last transmitted bit, in seconds. + std::map arrivalEndTimes; // Radio ID to last received bit, in seconds. +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc b/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc index 1366587e9fb..48fe60a49d6 100644 --- a/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc +++ b/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc @@ -656,6 +656,40 @@ const ITransmission *RadioMedium::getTransmission(int id) const return communicationCache->getTransmission(id); } +TransmissionTimingSnapshot RadioMedium::getTransmissionTimingSnapshot(int id) const +{ + TransmissionTimingSnapshot snapshot; + auto transmission = communicationCache->getTransmission(id); + if (!transmission) + return snapshot; + snapshot.transmissionId = transmission->getId(); + snapshot.transmitterRadioId = transmission->getTransmitterRadioId(); + snapshot.startTime = transmission->getStartTime(); + snapshot.endTime = transmission->getEndTime(); + communicationCache->mapRadios([&](const IRadio *radio) { + if (!radio || !radio->getReceiver() || radio->getId() == snapshot.transmitterRadioId) + return; + auto arrival = communicationCache->findCachedArrival(radio, transmission); + if (arrival) + snapshot.arrivalEndTimes[radio->getId()] = arrival->getEndTime(); + }); + return snapshot; +} + +std::optional RadioMedium::computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const +{ + auto current = communicationCache->getTransmission(currentTransmissionId); + if (!current || preceding.transmissionId < 0 || preceding.transmissionId == currentTransmissionId) + return std::nullopt; + if (current->getTransmitterRadioId() == preceding.transmitterRadioId) + return current->getStartTime() - preceding.endTime; + auto arrival = preceding.arrivalEndTimes.find(current->getTransmitterRadioId()); + if (arrival == preceding.arrivalEndTimes.end()) + return std::nullopt; + return current->getStartTime() - arrival->second; +} + const IListeningDecision *RadioMedium::listenOnMedium(const IRadio *radio, const IListening *listening) const { Enter_Method("listenOnMedium"); diff --git a/src/inet/physicallayer/wireless/common/medium/RadioMedium.h b/src/inet/physicallayer/wireless/common/medium/RadioMedium.h index 3952b247ddd..659b86c067e 100644 --- a/src/inet/physicallayer/wireless/common/medium/RadioMedium.h +++ b/src/inet/physicallayer/wireless/common/medium/RadioMedium.h @@ -337,6 +337,9 @@ class INET_API RadioMedium : public SimpleModule, public cListener, public IRadi virtual IWirelessSignal *transmitPacket(const IRadio *transmitter, Packet *packet) override; virtual Packet *receivePacket(const IRadio *receiver, IWirelessSignal *signal) override; virtual const ITransmission *getTransmission(int id) const override; + virtual TransmissionTimingSnapshot getTransmissionTimingSnapshot(int id) const override; + virtual std::optional computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const override; virtual const IListeningDecision *listenOnMedium(const IRadio *receiver, const IListening *listening) const override; diff --git a/tests/unit/TransmissionTimingSnapshot_1.test b/tests/unit/TransmissionTimingSnapshot_1.test new file mode 100644 index 00000000000..ff99ca1b5e5 --- /dev/null +++ b/tests/unit/TransmissionTimingSnapshot_1.test @@ -0,0 +1,138 @@ +%description: +Check shared transmission timing without a WLAN consumer or a propagation model. +Use generic transmissions and cached arrivals with exact endpoints. +Require snapshots to retain their values after cache expiry deletes the original objects. +Check positive, zero, and negative gaps for the same transmitter and different transmitters. +Check missing transmissions, missing arrivals, and the same transmission identity. +Require absent arrival queries to leave all three cache implementations unchanged before their first transmission. +This unit test does not establish physical reception or interframe protocol behavior. + +%includes: +#include +#include "inet/physicallayer/wireless/common/antenna/IsotropicAntenna.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/TransmissionBase.h" +#include "inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h" +#include "inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h" +#include "inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h" +#include "inet/physicallayer/wireless/common/medium/RadioMedium.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/Radio.h" +#include "inet/physicallayer/wireless/common/signal/Arrival.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TimingMedium : public RadioMedium { + public: + void attach(ICommunicationCache *cache) { communicationCache = cache; } +}; + +// These adapters supply identity and component references. No radio procedure executes. +class TimingRadio : public Radio { + int identity; + const IRadioMedium *attachedMedium; + const IAntenna *attachedAntenna; + const IReceiver *attachedReceiver; + public: + TimingRadio(int identity, const IRadioMedium *medium, const IAntenna *antenna, const IReceiver *receiver) : + identity(identity), attachedMedium(medium), attachedAntenna(antenna), attachedReceiver(receiver) {} + int getId() const override { return identity; } + const IRadioMedium *getMedium() const override { return attachedMedium; } + const IAntenna *getAntenna() const override { return attachedAntenna; } + const IReceiver *getReceiver() const override { return attachedReceiver; } +}; + +class TimingCache : public MapCommunicationCache { + public: + size_t receptionSlots() const { + size_t count = 0; + for (const auto& entry : transmissionCache) + if (entry.second.receptionCacheEntries) + count += entry.second.receptionCacheEntries->size(); + return count; + } +}; + +static TransmissionBase *makeTiming(const IRadio *radio, const Packet *packet, int startUs, int endUs) +{ + return new TransmissionBase(radio, packet, SimTime(startUs, SIMTIME_US), SimTime(endUs, SIMTIME_US), + SIMTIME_ZERO, SIMTIME_ZERO, SimTime(endUs - startUs, SIMTIME_US), Coord(), Coord(), + Quaternion(), Quaternion(), nullptr, nullptr, nullptr, nullptr, nullptr); +} + +template +static void checkAbsentArrival(const IRadio *radio, const ITransmission *transmission) +{ + Cache cache; + ASSERT(cache.getNumRadios() == 0 && cache.getNumTransmissions() == 0); + for (int i = 0; i < 3; i++) + ASSERT(cache.findCachedArrival(radio, transmission) == nullptr); + ASSERT(cache.getNumRadios() == 0 && cache.getNumTransmissions() == 0); +} + +%activity: +Packet packet("timing-input"); +IsotropicAntenna antenna; +FlatReceiverBase receiver; +TimingMedium medium; +TimingRadio transmitter(1, &medium, &antenna, &receiver); +TimingRadio recipient(2, &medium, &antenna, &receiver); +TimingRadio absentArrival(3, &medium, &antenna, &receiver); +TimingCache cache; +medium.attach(&cache); +auto previous = std::unique_ptr(makeTiming(&transmitter, &packet, 100, 140)); +checkAbsentArrival(&recipient, previous.get()); +checkAbsentArrival(&recipient, previous.get()); +checkAbsentArrival(&recipient, previous.get()); +int previousId = previous->getId(); +ASSERT(medium.getTransmissionTimingSnapshot(previousId).transmissionId == -1); +ASSERT(!medium.computePrecedingSignalGap(previousId, TransmissionTimingSnapshot()).has_value()); +cache.addRadio(&transmitter); +cache.addRadio(&recipient); +cache.addRadio(&absentArrival); +auto previousPointer = previous.release(); +cache.addTransmission(previousPointer); +cache.setCachedArrival(&recipient, previousPointer, new Arrival(SimTime(7, SIMTIME_US), SimTime(7, SIMTIME_US), + SimTime(107, SIMTIME_US), SimTime(147, SIMTIME_US), SIMTIME_ZERO, SIMTIME_ZERO, + SimTime(40, SIMTIME_US), Coord(), Coord(), Quaternion(), Quaternion())); +auto snapshot = medium.getTransmissionTimingSnapshot(previousId); +ASSERT(snapshot.transmissionId == previousId && snapshot.transmitterRadioId == 1); +ASSERT(snapshot.startTime == SimTime(100, SIMTIME_US) && snapshot.endTime == SimTime(140, SIMTIME_US)); +ASSERT(snapshot.arrivalEndTimes.size() == 1 && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +ASSERT(cache.findCachedArrival(&absentArrival, previousPointer) == nullptr); +ASSERT(cache.receptionSlots() == 1); +cache.setCachedInterferenceEndTime(previousPointer, SimTime(160, SIMTIME_US)); +wait(SimTime(200, SIMTIME_US)); +int expired = 0; +cache.removeNonInterferingTransmissions([&](const ITransmission *transmission) { + ASSERT(transmission->getId() == previousId); + expired++; +}); +ASSERT(expired == 1 && cache.getNumTransmissions() == 0); +ASSERT(medium.getTransmissionTimingSnapshot(previousId).transmissionId == -1); +ASSERT(!medium.computePrecedingSignalGap(previousId, snapshot).has_value()); +ASSERT(snapshot.endTime == SimTime(140, SIMTIME_US) && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +for (const TimingRadio *source : {&transmitter, &recipient}) { + int precedingEndUs = source == &transmitter ? 140 : 147; + for (int gapUs : {10, 0, -5}) { + auto current = makeTiming(source, &packet, precedingEndUs + gapUs, precedingEndUs + gapUs + 10); + cache.addTransmission(current); + auto gap = medium.computePrecedingSignalGap(current->getId(), snapshot); + ASSERT(gap.has_value() && *gap == SimTime(gapUs, SIMTIME_US)); + ASSERT(!medium.computePrecedingSignalGap(current->getId(), TransmissionTimingSnapshot()).has_value()); + auto sameId = snapshot; + sameId.transmissionId = current->getId(); + ASSERT(!medium.computePrecedingSignalGap(current->getId(), sameId).has_value()); + auto noArrival = snapshot; + noArrival.arrivalEndTimes.clear(); + auto withoutArrival = medium.computePrecedingSignalGap(current->getId(), noArrival); + ASSERT(source == &transmitter ? withoutArrival.has_value() : !withoutArrival.has_value()); + cache.removeTransmission(current); + } +} +ASSERT(snapshot.transmissionId == previousId && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +EV << "Shared transmission timing snapshot checks passed\n"; + +%contains: stdout +Shared transmission timing snapshot checks passed From e6d0d166e5e2634b11d3294f21f43704b4be58f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:48:34 +0200 Subject: [PATCH 08/16] ieee80211: fix: include the HT signal extension at 2.4 GHz Before this commit, HT duration omits the six-microsecond signal extension at 2.4 GHz. A waveform therefore ends early and supplies incorrect airtime and interval evidence. After this commit, HT duration includes that extension. Mode queries supply its nominal duration. Transmission queries derive the actual extension from the waveform so interval consumers cannot count it twice. The mode fixture checks the 2.4 GHz extension and the other-band control. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.physicallayer.wireless.ieee80211 | behavior.change.fix | test whatsnew migration | ieee80211-compressed-blockack-replacement --- .../development/ieee80211-signal-extension.rst | 4 ++++ .../incompatible/ieee80211-signal-extension.txt | 3 +++ .../wireless/ieee80211/mode/IIeee80211Mode.h | 2 ++ .../wireless/ieee80211/mode/IIeee80211Mode.msg | 1 + .../wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h | 3 ++- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 4 +++- .../wireless/ieee80211/mode/Ieee80211ModeBase.h | 1 + .../ieee80211/packetlevel/Ieee80211Transmission.cc | 12 ++++++++++++ .../ieee80211/packetlevel/Ieee80211Transmission.h | 5 +++++ tests/unit/Ieee80211HtModeSet_1.test | 8 ++++++++ 10 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 doc/src/migration-guide/development/ieee80211-signal-extension.rst create mode 100644 doc/whatsnew/incompatible/ieee80211-signal-extension.txt diff --git a/doc/src/migration-guide/development/ieee80211-signal-extension.rst b/doc/src/migration-guide/development/ieee80211-signal-extension.rst new file mode 100644 index 00000000000..32a97257371 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-signal-extension.rst @@ -0,0 +1,4 @@ +IEEE 802.11 signal extension +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Custom IEEE 802.11 modes must implement ``getSignalExtensionTime()``. ``Ieee80211ModeBase`` supplies zero. ERP and 2.4 GHz HT modes supply their six-microsecond extension. HT duration now includes that extension, which can change airtime and simulation results. The transmission query derives the actual extension from its waveform, so consumers must not add the nominal value again. diff --git a/doc/whatsnew/incompatible/ieee80211-signal-extension.txt b/doc/whatsnew/incompatible/ieee80211-signal-extension.txt new file mode 100644 index 00000000000..20cf68f75f4 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-signal-extension.txt @@ -0,0 +1,3 @@ +IEEE 802.11 signal extension + +HT duration at 2.4 GHz now includes its six-microsecond signal extension. Previously, that waveform ended early, which could change interframe recognition and airtime. Custom IEEE 802.11 modes must supply the new nominal-extension query. A transmission query supplies the actual extension from its waveform so consumers do not count it twice. diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index cecad21b7ad..4224c730c4e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -83,6 +83,8 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject virtual const simtime_t getSlotTime() const = 0; virtual const simtime_t getSifsTime() const = 0; virtual const simtime_t getRifsTime() const = 0; + // Default extension after the PPDU, in seconds. A received waveform can omit it for RIFS. + virtual const simtime_t getSignalExtensionTime() const = 0; virtual const simtime_t getCcaTime() const = 0; virtual const simtime_t getPhyRxStartDelay() const = 0; virtual const simtime_t getRxTxTurnaroundTime() const = 0; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg index 52c42112f65..425faaaa0cd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg @@ -57,6 +57,7 @@ class IIeee80211Mode extends cObject simtime_t slotTime; simtime_t sifsTime; simtime_t rifsTime; + simtime_t signalExtensionTime; simtime_t ccaTime; simtime_t phyRxStartDelay; simtime_t rxTxTurnaroundTime; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h index e5c644ff0fc..e0f11ce9eba 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h @@ -33,7 +33,8 @@ class INET_API Ieee80211ErpOfdmMode : public Ieee80211OfdmMode virtual const simtime_t getSifsTime() const override { return 10E-6; } // For ERP-OFDM modes, an ERP packet is followed by a period of no transmission // with a length of 6 μs called the signal extension. - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + signalMode->getDuration() + dataMode->getDuration(dataLength) + 6E-6; } + virtual const simtime_t getSignalExtensionTime() const override { return SimTime(6, SIMTIME_US); } + virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + signalMode->getDuration() + dataMode->getDuration(dataLength) + getSignalExtensionTime(); } }; class INET_API Ieee80211ErpOfdmCompliantModes diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 62bc6aa98c0..ee6c0a76685 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -258,6 +258,8 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual const simtime_t getShortSlotTime() const; virtual const simtime_t getSifsTime() const override; virtual const simtime_t getRifsTime() const override { return 2E-6; } + // IEEE Std 802.11-2024, Table 19-25: the 2.4 GHz HT PPDU includes a 6 us extension. + virtual const simtime_t getSignalExtensionTime() const override { return centerFrequencyMode == BAND_2_4GHZ ? SimTime(6, SIMTIME_US) : SIMTIME_ZERO; } virtual const simtime_t getCcaTime() const override { return 4E-6; } // < 4 virtual const simtime_t getPhyRxStartDelay() const override { return 24E-6; } virtual const simtime_t getRxTxTurnaroundTime() const override { return 2E-6; } // < 2 @@ -266,7 +268,7 @@ 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 { return preambleMode->getDuration() + dataMode->getDuration(dataLength) + getSignalExtensionTime(); } }; // A specification of the high-throughput (HT) physical layer (PHY) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h index 36edd5ba0e7..39b7af724dd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h @@ -29,6 +29,7 @@ class INET_API Ieee80211ModeBase : public IIeee80211Mode virtual PreambleType getLegacyPreambleType() const override { return PreambleType::UNKNOWN; } virtual int getHtMcsIndex() const override { return -1; } virtual bool isHtShortGuardInterval() const override { return false; } + virtual const simtime_t getSignalExtensionTime() const override { return SIMTIME_ZERO; } virtual const char *getName() const override { return name.c_str(); } }; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc index 4f1be224443..a5eac8b7868 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc @@ -6,6 +6,7 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" namespace inet { @@ -27,6 +28,17 @@ std::ostream& Ieee80211Transmission::printToStream(std::ostream& stream, int lev return TransmissionBase::printToStream(stream, level); } +std::optional Ieee80211Transmission::getSignalExtensionDuration() const +{ + auto header = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(getPacket()); + auto defaultExtension = mode->getSignalExtensionTime(); + // The PHY Length field contains MAC octets. The mode duration already includes PHY headers and padding. + auto extension = getEndTime() - getStartTime() - mode->getDuration(B(header->getLengthField())) + defaultExtension; + if (extension == SIMTIME_ZERO || extension == defaultExtension) + return extension; + return std::nullopt; +} + } // namespace physicallayer } // namespace inet diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h index 2bf3eae2a82..f1a9e9bbce1 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211TRANSMISSION_H #define __INET_IEEE80211TRANSMISSION_H +#include + #include "inet/physicallayer/wireless/common/base/packetlevel/TransmissionBase.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" @@ -30,6 +32,9 @@ class INET_API Ieee80211Transmission : public TransmissionBase virtual const IIeee80211Mode *getMode() const { return mode; } virtual const Ieee80211Channel *getChannel() const { return channel; } + // The actual waveform can omit the default extension through NO_SIG_EXTN for RIFS. + // An unsupported duration returns no value; the method changes no state. + std::optional getSignalExtensionDuration() const; }; } // namespace physicallayer diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 0dc059d2d40..552c8f54549 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -235,6 +235,14 @@ for (int mcs = 0; mcs < 32; mcs++) { ASSERT(typedHt->getSlotTime() == SimTime(20, SIMTIME_US)); ASSERT(typedAcHt->getSifsTime() == SimTime(16, SIMTIME_US)); ASSERT(typedAcHt->getSlotTime() == SimTime(9, SIMTIME_US)); + ASSERT(typedHt->getSignalExtensionTime() == SimTime(6, SIMTIME_US)); + ASSERT(typedAcHt->getSignalExtensionTime() == SIMTIME_ZERO); + ASSERT(typedHt->getDuration(B(100)) - typedAcHt->getDuration(B(100)) == SimTime(6, SIMTIME_US)); + if (mcs == 0 && width == MHz(20) && gi == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) { + // MCS 0: 36 us preamble + ceil((16 + 800 + 6) / 26) * 4 us + band extension. + ASSERT(typedHt->getDuration(B(100)) == SimTime(170, SIMTIME_US)); + ASSERT(typedAcHt->getDuration(B(100)) == SimTime(164, SIMTIME_US)); + } ASSERT(acHt->getDataMode()->getNetBitrate() == ht->getDataMode()->getNetBitrate()); ASSERT(acHt->getDataMode()->getNumberOfSpatialStreams() == ht->getDataMode()->getNumberOfSpatialStreams()); ASSERT(vhtOnlyModeSet->getIsMandatory(acHt) == htModeSet->getIsMandatory(ht)); From 12f37da806396580f4ed1afdddfef3cc1dd296bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:48:50 +0200 Subject: [PATCH 09/16] ieee80211: fix: preserve advertised RIFS permission Before this commit, HT management conversion loses RIFS Mode. A recipient therefore cannot use received permission to identify a permitted RIFS relation. After this commit, HT Operation retains RIFS Mode through conversion and equality checks. The MIB permits RIFS for unprotected HT operation and clears the permission for VHT. For example, an unprotected HT operation survives conversion with permission intact. The capability fixture checks these states. This commit adds no RIFS transmission scheduler. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change.fix | test whatsnew migration | ieee80211-compressed-blockack-replacement --- .../development/ieee80211-rifs-permission.rst | 4 ++++ .../incompatible/ieee80211-rifs-permission.txt | 3 +++ .../ieee80211/mgmt/Ieee80211HtMgmtElements.h | 2 ++ .../ieee80211/mib/Ieee80211HtCapabilities.h | 3 ++- src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc | 6 ++++++ tests/unit/Ieee80211HtCapabilities_1.test | 12 ++++++++++++ 6 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 doc/src/migration-guide/development/ieee80211-rifs-permission.rst create mode 100644 doc/whatsnew/incompatible/ieee80211-rifs-permission.txt diff --git a/doc/src/migration-guide/development/ieee80211-rifs-permission.rst b/doc/src/migration-guide/development/ieee80211-rifs-permission.rst new file mode 100644 index 00000000000..21ec762c4f3 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-rifs-permission.rst @@ -0,0 +1,4 @@ +IEEE 802.11 RIFS permission +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +HT Operation now preserves RIFS Mode through management conversion and equality checks. VHT operation clears that permission. The change preserves the received permission for a consumer that checks RIFS relations; it adds no RIFS transmission scheduler. diff --git a/doc/whatsnew/incompatible/ieee80211-rifs-permission.txt b/doc/whatsnew/incompatible/ieee80211-rifs-permission.txt new file mode 100644 index 00000000000..a937f0a1000 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-rifs-permission.txt @@ -0,0 +1,3 @@ +IEEE 802.11 RIFS permission + +HT Operation now preserves RIFS Mode through management conversion and equality checks. The MIB permits RIFS under unprotected HT operation and clears the permission for VHT. A recipient can therefore check the actual advertised permission before it recognizes a RIFS relation. This change adds no RIFS transmission scheduler. diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h index 6a851b507e4..fb62285460e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h @@ -101,6 +101,7 @@ inline Ieee80211HtOperationElement makeHtOperationElement(const physicallayer::I element.secondaryChannelOffset = operation.secondaryChannelOffset; element.staChannelWidth40Mhz = operation.operatingChannelWidth == MHz(40); element.protectionMode = static_cast(operation.protectionMode); + element.rifsMode = operation.rifsMode; for (int i = 0; i < 77; i++) element.basicMcsSupported[i] = operation.basicMcsSupported[i]; return element; @@ -119,6 +120,7 @@ inline Ieee80211HtOperation makeHtOperation(const physicallayer::IIeee80211Band operation.secondaryChannelOffset = element.secondaryChannelOffset; operation.operatingChannelWidth = element.staChannelWidth40Mhz ? MHz(40) : MHz(20); operation.protectionMode = static_cast(element.protectionMode); + operation.rifsMode = element.rifsMode; for (int i = 0; i < 77; i++) operation.basicMcsSupported[i] = element.basicMcsSupported[i]; return operation; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index b2b02ad5a4d..26a27315c11 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -70,12 +70,13 @@ struct Ieee80211HtOperation int primaryChannel = 0; int secondaryChannelOffset = 0; Ieee80211HtProtectionMode protectionMode = Ieee80211HtProtectionMode::NO_PROTECTION; + bool rifsMode = false; 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; + rifsMode == other.rifsMode && basicMcsSupported == other.basicMcsSupported; } }; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 9c4db7b2026..f57837295d2 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -162,8 +162,10 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS modeSet->isHtShortGuardIntervalSupported(MHz(20)); localHtCapabilities.shortGi40 = localHtCapabilities.supportedChannelWidths.count(MHz(40)) != 0 && modeSet->isHtShortGuardIntervalSupported(MHz(40)); + bool vhtSupported = false; for (int index = 0; index < modeSet->getNumModes(); index++) { const auto *mode = modeSet->getMode(index); + vhtSupported |= mode->getModulationClass() == physicallayer::IIeee80211Mode::ModulationClass::VHT; int mcs = mode->getHtMcsIndex(); if (mcs >= 0 && mcs < 77 && operationalChannelWidths.count(mode->getDataMode()->getBandwidth()) != 0 && mode->getDataMode()->getNumberOfSpatialStreams() <= operationalHtSpatialStreamLimit) @@ -199,6 +201,10 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS if (protectionMode < 0 || protectionMode > 3) throw cRuntimeError("htProtectionMode must be between 0 and 3"); htOperation.protectionMode = static_cast(protectionMode); + // IEEE Std 802.11-2024, 10.27.3.3: the protected modes permit the conservative zero policy. + // IEEE Std 802.11-2024, 11.38.1: a VHT AP advertises zero. + htOperation.rifsMode = !vhtSupported && (htOperation.protectionMode == Ieee80211HtProtectionMode::NO_PROTECTION || + htOperation.protectionMode == Ieee80211HtProtectionMode::TWENTY_MHZ_PROTECTION); bool peerStateChanged = false; for (auto& entry : peerHtStates) { if (entry.second.valid) { diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index dee6a669f79..455332ef964 100644 --- a/tests/unit/Ieee80211HtCapabilities_1.test +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -33,6 +33,18 @@ peer.maxAmpduLengthExponent = 3; Ieee80211HtOperation operation; operation.operatingChannelWidth = MHz(20); +for (bool permitted : {false, true}) { + auto advertised = operation; + advertised.primaryChannel = 0; + advertised.rifsMode = permitted; + auto band = physicallayer::Ieee80211CompliantBands::getBand("2.4 GHz"); + auto element = makeHtOperationElement(band, advertised); + ASSERT(element.rifsMode == permitted); + auto received = makeHtOperation(band, element); + ASSERT(received == advertised); + received.rifsMode = !permitted; + ASSERT(!(received == advertised)); +} operation.basicMcsSupported[0] = true; auto negotiated = negotiateHtCapabilities(local, peer, operation); From ec5dc350bfceb7233f61912e680f0168d3cdcf47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:49:00 +0200 Subject: [PATCH 10/16] ieee80211: fix: release temporary Block Ack bitmap allocations Before this commit, Basic and Compressed decoders allocate temporary BitVector objects and copy their values without deletion. Repeated Block Ack decode therefore loses those allocations. After this commit, the decoders construct bitmap values directly. The generated fields retain their own values without an unowned temporary. For example, independent Basic and Compressed octets retain the same bitmap bits after decode. The direct wire check covers both paths. Type interpretation and frame acceptance remain unchanged. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change.fix | test whatsnew | ieee80211-compressed-blockack-replacement --- .../ieee80211-block-ack-bitmap-ownership.txt | 3 ++ .../mac/Ieee80211MacHeaderSerializer.cc | 5 ++- tests/unit/Ieee80211BlockAckWire_1.test | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt create mode 100644 tests/unit/Ieee80211BlockAckWire_1.test diff --git a/doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt b/doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt new file mode 100644 index 00000000000..ae5a041de64 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt @@ -0,0 +1,3 @@ +Block Ack bitmap ownership + +Block Ack decoders now use bitmap values without unowned temporary allocations. Repeated Basic and Compressed decode no longer loses those temporary objects. diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index 2a482cded2f..d841c8be5aa 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -664,8 +664,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 +683,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/tests/unit/Ieee80211BlockAckWire_1.test b/tests/unit/Ieee80211BlockAckWire_1.test new file mode 100644 index 00000000000..0cd32ee3a46 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckWire_1.test @@ -0,0 +1,36 @@ +%description: +Check Basic and Compressed bitmap values after independent byte decode. +The decoder must preserve the same bitmap contents without unowned temporary objects. +Type validation remains outside this prerequisite. + +%includes: +#include "inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.h" + +using namespace inet; +using namespace inet::ieee80211; + +%activity: +for (bool compressed : {false, true}) { + std::vector bytes(compressed ? 28 : 148, 0); + bytes[0] = 0x94; + bytes[16] = compressed ? 4 : 0; + bytes[20] = 1; + MemoryInputStream stream(bytes); + auto decoded = Chunk::deserialize(stream, typeid(Ieee80211MacHeader)); + if (compressed) { + auto response = dynamicPtrCast(decoded); + ASSERT(response && response->getBlockAckBitmap().getSize() == 64); + ASSERT(response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(63)); + } + else { + auto response = dynamicPtrCast(decoded); + ASSERT(response && response->getBlockAckBitmapArraySize() == 64); + ASSERT(response->getBlockAckBitmap(0).getBit(0)); + ASSERT(!response->getBlockAckBitmap(63).getBit(15)); + } +} +EV << "Bitmap decode checks passed.\n"; + +%contains: stdout +Bitmap decode checks passed. From 9bef5f1048d093e87c26aa37ac18d7fbdb930acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:49:54 +0200 Subject: [PATCH 11/16] ieee80211: add: serialize Basic and Compressed Block Ack Types Before this commit, Block Ack serialization decodes only part of the Type field. Compressed bitmap and fragment fields lack complete wire validation. The preceding prerequisite already removes the unowned bitmap temporaries. After this commit, the serializer decodes all four Type bits. Type 0 retains Basic compatibility. Type 2 supplies a 64-bit Compressed bitmap and a 32-byte response with FCS. Both BAR profiles occupy 24 bytes with FCS. Invalid Types and malformed fragment fields fail validation. For example, Type 2 with a nonzero fragment field fails validation instead of entering the supported Compressed profile. Independent octets cover both Types, malformed fields, reserved bits, lengths, and sequence wrap. The existing Compressed BAR builder supplies its complete header. Synthetic fixtures provide valid FCS lengths and every Basic bitmap entry. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- .../ieee80211-block-ack-wire-producers.rst | 6 + .../ieee80211-block-ack-bitmap-ownership.txt | 3 - .../ieee80211-block-ack-wire-types.txt | 3 + .../ieee80211/mac/Ieee80211Frame.msg | 8 +- .../mac/Ieee80211MacHeaderSerializer.cc | 40 ++- .../blockack/OriginatorBlockAckProcedure.cc | 12 +- .../Ieee80211BlockAckActionDispatch_1.test | 1 + ...Ieee80211BlockAckFragmentRetirement_1.test | 2 + tests/serializer/REMAINING_GAPS.md | 4 + .../lib/fillers/ChunkFillers_ieee80211.cc | 25 +- .../serializer_chunk_roundtrip.test | 13 +- tests/unit/Ieee80211BlockAckWire_1.test | 238 ++++++++++++++++-- 12 files changed, 288 insertions(+), 67 deletions(-) create mode 100644 doc/src/migration-guide/development/ieee80211-block-ack-wire-producers.rst delete mode 100644 doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt create mode 100644 doc/whatsnew/incompatible/ieee80211-block-ack-wire-types.txt diff --git a/doc/src/migration-guide/development/ieee80211-block-ack-wire-producers.rst b/doc/src/migration-guide/development/ieee80211-block-ack-wire-producers.rst new file mode 100644 index 00000000000..203fcdce589 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-block-ack-wire-producers.rst @@ -0,0 +1,6 @@ +IEEE 802.11 Block Ack wire producers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The serializer decodes the complete four-bit Type field. Type 0 retains Basic compatibility; Type 2 selects Compressed. Other Types produce invalid input. Basic producers must keep the two low bits of their reserved field zero because those bits overlap Type. The serializer rejects inconsistent Basic objects. Compressed reserved fields write zero; received reserved values do not affect the decoded result. + +Compressed producers must supply Fragment Number zero and a 64-bit bitmap. A Compressed Block Ack header chunk contains 28 bytes before FCS. Both BAR profiles occupy 24 bytes with FCS. Keep the corresponding header chunk and trailer lengths consistent. The serializer rejects malformed wire fields before runtime processing. diff --git a/doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt b/doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt deleted file mode 100644 index ae5a041de64..00000000000 --- a/doc/whatsnew/incompatible/ieee80211-block-ack-bitmap-ownership.txt +++ /dev/null @@ -1,3 +0,0 @@ -Block Ack bitmap ownership - -Block Ack decoders now use bitmap values without unowned temporary allocations. Repeated Basic and Compressed decode no longer loses those temporary objects. diff --git a/doc/whatsnew/incompatible/ieee80211-block-ack-wire-types.txt b/doc/whatsnew/incompatible/ieee80211-block-ack-wire-types.txt new file mode 100644 index 00000000000..df9ab23bb26 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-block-ack-wire-types.txt @@ -0,0 +1,3 @@ +IEEE 802.11 Block Ack wire types + +The serializer supports the complete four-bit Type field for Basic compatibility and Compressed frames. Compressed responses contain a 64-bit bitmap and occupy 32 bytes with FCS. Both BAR profiles occupy 24 bytes with FCS. Basic producers must keep reserved bits that overlap Type zero. Invalid Types and malformed fragment fields now fail validation instead of ambiguous decoding. diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg index e139deaf75d..7d1367944d4 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg @@ -43,6 +43,8 @@ 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 + control + sequence + bitmap + FCS +const b LENGTH_BLOCKACKREQ = B(16 + 2 + 2 + 4); // header + control + sequence + 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; @@ -356,7 +358,7 @@ class Ieee80211BlockAckReq extends Ieee80211TwoAddressHeader class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(16 + 2 + 2); // header + BAR control + BAR information + chunkLength = LENGTH_BLOCKACKREQ - B(4); int tidInfo @bit(4); // The TID_INFO subfield of the BAR Control field of the Basic BlockAckReq frame contains the TID for which a Basic BlockAck frame is requested. @@ -373,7 +375,7 @@ class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq class Ieee80211CompressedBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(16 + 2 + 2); // header + BAR control + BAR information + chunkLength = LENGTH_BLOCKACKREQ - B(4); int tidInfo @bit(4); // The TID_INFO subfield of the BAR Control field of the Compressed BlockAckReq frame contains the TID for which a BlockAck frame is requested. // The BAR Information field of the Compressed BlockAckReq frame contains the Block Ack Starting @@ -455,7 +457,7 @@ class Ieee80211BasicBlockAck extends Ieee80211BlockAck // class Ieee80211CompressedBlockAck extends Ieee80211BlockAck { - // chunkLength TODO + chunkLength = LENGTH_COMPRESSED_BLOCKACK - B(4); multiTid = 0; compressedBitmap = 1; diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index d841c8be5aa..d2b1ae642fe 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -331,10 +331,13 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeMacAddress(blockAckReq->getTransmitterAddress()); bool multiTid = blockAckReq->getMultiTid(); bool compressedBitmap = blockAckReq->getCompressedBitmap(); - // BAR Control is a 16-bit little-endian field: bit 0 BAR Ack Policy, bit 1 - // Multi-TID, bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO + // IEEE Std 802.11-2024, 9.3.1.7: B1-B4 encode the complete BAR Type. + if (!multiTid && !compressedBitmap && (blockAckReq->getReserved() & 3)) + throw cRuntimeError("Basic BAR reserved fields must encode Type 0"); uint16_t barControl = (blockAckReq->getBarAckPolicy() ? 1 : 0) | ((multiTid ? 1 : 0) << 1) | ((compressedBitmap ? 1 : 0) << 2) | ((blockAckReq->getReserved() & 0x1FF) << 3); + if (!multiTid && compressedBitmap) + barControl = 2 << 1; if (!multiTid && !compressedBitmap) { auto basicBlockAckReq = CHK(dynamicPtrCast(chunk)); stream.writeUint16Le(barControl | ((basicBlockAckReq->getTidInfo() & 0xF) << 12)); @@ -347,6 +350,8 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c } else if (!multiTid && compressedBitmap) { auto compressedBlockAckReq = CHK(dynamicPtrCast(chunk)); + if (compressedBlockAckReq->getFragmentNumber() != 0) + throw cRuntimeError("Compressed BAR Fragment Number must be zero"); stream.writeUint16Le(barControl | ((compressedBlockAckReq->getTidInfo() & 0xF) << 12)); writeSequenceControl(stream, compressedBlockAckReq->getFragmentNumber(), compressedBlockAckReq->getStartingSequenceNumber().get()); if (stream.getLength() - startPos != compressedBlockAckReq->getChunkLength()) @@ -367,10 +372,13 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeMacAddress(blockAck->getTransmitterAddress()); bool multiTid = blockAck->getMultiTid(); bool compressedBitmap = blockAck->getCompressedBitmap(); - // BA Control is a 16-bit little-endian field: bit 0 Ack Policy, bit 1 Multi-TID, - // bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO + // IEEE Std 802.11-2024, 9.3.1.8: B1-B4 encode the complete BA Type. + if (!multiTid && !compressedBitmap && (blockAck->getReserved() & 3)) + throw cRuntimeError("Basic Block Ack reserved fields must encode Type 0"); uint16_t baControl = (blockAck->getBlockAckPolicy() ? 1 : 0) | ((multiTid ? 1 : 0) << 1) | ((compressedBitmap ? 1 : 0) << 2) | ((blockAck->getReserved() & 0x1FF) << 3); + if (!multiTid && compressedBitmap) + baControl = 2 << 1; if (!multiTid && !compressedBitmap) { auto basicBlockAck = CHK(dynamicPtrCast(chunk)); stream.writeUint16Le(baControl | ((basicBlockAck->getTidInfo() & 0xF) << 12)); @@ -386,6 +394,10 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c } else if (!multiTid && compressedBitmap) { auto compressedBlockAck = CHK(dynamicPtrCast(chunk)); + if (compressedBlockAck->getFragmentNumber() != 0) + throw cRuntimeError("Compressed Block Ack Fragment Number must be zero"); + if (compressedBlockAck->getBlockAckBitmap().getSize() != 64) + throw cRuntimeError("Compressed Block Ack bitmap must contain 64 bits"); stream.writeUint16Le(baControl | ((compressedBlockAck->getTidInfo() & 0xF) << 12)); // Block Ack Starting Sequence Control = 4-bit Fragment Number (reserved) + 12-bit sequence number writeSequenceControl(stream, compressedBlockAck->getFragmentNumber(), compressedBlockAck->getStartingSequenceNumber().get()); @@ -600,6 +612,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAckReq->setReceiverAddress(stream.readMacAddress()); blockAckReq->setTransmitterAddress(stream.readMacAddress()); uint16_t barControl = stream.readUint16Le(); + auto barType = (barControl >> 1) & 0xF; bool multiTid = (barControl >> 1) & 1; bool compressedBitmap = (barControl >> 2) & 1; blockAckReq->setBarAckPolicy(barControl & 1); @@ -608,7 +621,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAckReq->setReserved((barControl >> 3) & 0x1FF); int fragmentNumber = 0; SequenceNumberCyclic startingSequenceNumber; - if (!multiTid && !compressedBitmap) { + if (barType == 0) { auto basicBlockAckReq = makeShared(); copyBasicFields(basicBlockAckReq, macHeader); copyBlockAckReqFrameFields(basicBlockAckReq, blockAckReq); @@ -618,14 +631,18 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre basicBlockAckReq->setStartingSequenceNumber(startingSequenceNumber); return basicBlockAckReq; } - else if (!multiTid && compressedBitmap) { + else if (barType == 2) { auto compressedBlockAckReq = makeShared(); copyBasicFields(compressedBlockAckReq, macHeader); copyBlockAckReqFrameFields(compressedBlockAckReq, blockAckReq); + compressedBlockAckReq->setBarAckPolicy(false); + compressedBlockAckReq->setReserved(0); compressedBlockAckReq->setTidInfo((barControl >> 12) & 0xF); readSequenceControl(stream, fragmentNumber, startingSequenceNumber); compressedBlockAckReq->setFragmentNumber(fragmentNumber); compressedBlockAckReq->setStartingSequenceNumber(startingSequenceNumber); + if (fragmentNumber != 0) + compressedBlockAckReq->markIncorrect(); return compressedBlockAckReq; } else @@ -640,9 +657,8 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAck->setDurationField(SimTime(stream.readUint16Le(), SIMTIME_US)); blockAck->setReceiverAddress(stream.readMacAddress()); blockAck->setTransmitterAddress(stream.readMacAddress()); - // BA Control is a 16-bit little-endian field: bit 0 Ack Policy, bit 1 Multi-TID, - // bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO uint16_t baControl = stream.readUint16Le(); + auto baType = (baControl >> 1) & 0xF; bool multiTid = (baControl >> 1) & 1; bool compressedBitmap = (baControl >> 2) & 1; uint8_t tidInfo = (baControl >> 12) & 0xF; @@ -650,7 +666,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAck->setMultiTid(multiTid); blockAck->setCompressedBitmap(compressedBitmap); blockAck->setReserved((baControl >> 3) & 0x1FF); - if (!multiTid && !compressedBitmap) { + if (baType == 0) { auto basicBlockAck = makeShared(); copyBasicFields(basicBlockAck, macHeader); copyBlockAckFrameFields(basicBlockAck, blockAck); @@ -668,10 +684,12 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre } return basicBlockAck; } - else if (!multiTid && compressedBitmap) { + else if (baType == 2) { auto compressedBlockAck = makeShared(); copyBasicFields(compressedBlockAck, macHeader); copyBlockAckFrameFields(compressedBlockAck, blockAck); + compressedBlockAck->setBlockAckPolicy(false); + compressedBlockAck->setReserved(0); compressedBlockAck->setTidInfo(tidInfo); // Block Ack Starting Sequence Control = 4-bit Fragment Number (reserved) + 12-bit @@ -684,6 +702,8 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre bytes.push_back(stream.readByte()); } compressedBlockAck->setBlockAckBitmap(BitVector(bytes)); + if (fragmentNumber != 0) + compressedBlockAck->markIncorrect(); return compressedBlockAck; } else { diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc index d02e412b644..ee1960c3fb0 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 diff --git a/tests/module/Ieee80211BlockAckActionDispatch_1.test b/tests/module/Ieee80211BlockAckActionDispatch_1.test index 1ec9db351e9..e09a34d1ab8 100644 --- a/tests/module/Ieee80211BlockAckActionDispatch_1.test +++ b/tests/module/Ieee80211BlockAckActionDispatch_1.test @@ -62,6 +62,7 @@ class ActionDispatchHcf : public Hcf { bar->setStartingSequenceNumber(SequenceNumberCyclic(104)); bar->setFragmentNumber(0); auto request = new Packet("bar-before-data", bar); + request->insertAtBack(makeShared()); request->addTag()->setMode(modeSet->getMode(bps(6000000))); processLowerFrame(request, bar); ASSERT(delivered == 0); diff --git a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test index 47377708224..306df0010d4 100644 --- a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test +++ b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test @@ -218,6 +218,8 @@ class FragmentRetirementHcf : public Hcf { for (int i = 1; i < 16; i++) bits.appendBit(false); header->setBlockAckBitmap(0, bits); + for (int position = 1; position < 64; position++) + header->setBlockAckBitmap(position, BitVector(std::vector{0, 0})); auto packet = new Packet("block-ack", header); packet->insertAtBack(makeShared()); packet->addTag()->setMode(modeSet->getMode(bps(24000000))); diff --git a/tests/serializer/REMAINING_GAPS.md b/tests/serializer/REMAINING_GAPS.md index c112225ab8d..78180de2e35 100644 --- a/tests/serializer/REMAINING_GAPS.md +++ b/tests/serializer/REMAINING_GAPS.md @@ -432,6 +432,10 @@ anyway. ## Untested serializers (registered, but no real-capture coverage) +The Block Ack producers retain Basic BAR and Block Ack with Type 0. Their `reserved` fields clear the two bits that overlap Type. The Compressed producers use Type 2 and leave ACK-policy and `reserved` fields at zero. These Compressed fields have no field-preserving wire contract. The serializer writes zero for reserved bits B0 and B5–B11 and ignores their received values. + +`Ieee80211BlockAckWire_1.test` checks independent octets for both variants and both frame types. It also checks unsupported Types, malformed Compressed fragment subfields, and response lengths. Basic response headers contain 148 bytes; Compressed response headers contain 28 bytes. The four-byte FCS gives MPDU lengths of 152 and 32 bytes. These checks do not establish a complete frame exchange or complete historical Basic conformance. + The tests cover different things. `serializer_chunk_roundtrip` **constructs** an instance of every registered chunk type — from hand-written typed fillers in `tests/serializer/lib/fillers/`, one or more per type, a variant per branch the serializer takes diff --git a/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc b/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc index 158b12c5620..163bfaa38c3 100644 --- a/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc +++ b/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc @@ -432,13 +432,8 @@ void addFillers_ieee80211(std::vector& fillers) setChunkLength(c, LENGTH_DELBA); }}); - // --- Block Ack request / response, Basic and Compressed variants: multiTid and - // compressedBitmap are the serializer's variant discriminator (see - // Ieee80211MacHeaderSerializer's ST_BLOCKACK_REQ/ST_BLOCKACK cases), pinned per - // subclass by the .msg -- kept at that pinned pair. fragmentNumber is the Fragment - // Number subfield of the Block Ack (Req) Starting Sequence Control, which 802.11 - // always sets to 0 in a Block Ack context (no fragmentation) -- kept at the .msg - // default in all four subtypes. + // The concrete classes fix the Type bits. Basic reserved values must retain Type 0. + // Compressed reserved fields write zero. All producers retain fragmentNumber = 0. fillers.push_back({"inet::ieee80211::Ieee80211BasicBlockAckReq", "", [](Chunk *c) { auto h = check_and_cast(c); FillValues v; @@ -451,7 +446,7 @@ void addFillers_ieee80211(std::vector& fillers) h->setMultiTid(false); // pinned by the class: selects the (non-multi-TID) Basic/Compressed branch h->setCompressedBitmap(false); // pinned by the class: selects the Basic (non-compressed) branch h->setBarAckPolicy(true); - h->setReserved(v.uint(9)); + h->setReserved(v.uint(9) & ~3u); h->setTidInfo((int)v.uint(4)); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); }}); @@ -466,8 +461,8 @@ void addFillers_ieee80211(std::vector& fillers) h->setType(ST_BLOCKACK_REQ); h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(true); // pinned by the class: selects the Compressed branch - h->setBarAckPolicy(true); - h->setReserved(v.uint(9)); + h->setBarAckPolicy(false); + h->setReserved(0); h->setTidInfo((int)v.uint(4)); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); }}); @@ -483,7 +478,7 @@ void addFillers_ieee80211(std::vector& fillers) h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(false); // pinned by the class: selects the Basic bitmap branch h->setBlockAckPolicy(true); - h->setReserved(v.uint(9)); + h->setReserved(v.uint(9) & ~3u); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); // The Block Ack bitmap: 64 elements of 2 bytes (16 bits) each; never leave any at // the default empty BitVector -- getBytes()[0]/[1] would read out of bounds. @@ -502,15 +497,11 @@ void addFillers_ieee80211(std::vector& fillers) h->setType(ST_BLOCKACK); h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(true); // pinned by the class: selects the Compressed bitmap branch - h->setBlockAckPolicy(true); - h->setReserved(v.uint(9)); + h->setBlockAckPolicy(false); + h->setReserved(0); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); h->setBlockAckBitmap(BitVector(std::vector{v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8()})); h->setTidInfo((int)v.uint(4)); - // the .msg leaves chunkLength unset ("TODO"); the wire layout is fixed at - // 16 (header) + 2 (BA control) + 2 (starting-sequence control) + 8 (compressed - // bitmap) = 28 bytes. - setChunkLength(c, B(28)); }}); // --- management frame bodies. Each is content-length-dependent (SSID, rates, the diff --git a/tests/serializer/serializer_chunk_roundtrip.test b/tests/serializer/serializer_chunk_roundtrip.test index 18fa3f2e235..3ab712fe87c 100644 --- a/tests/serializer/serializer_chunk_roundtrip.test +++ b/tests/serializer/serializer_chunk_roundtrip.test @@ -420,11 +420,10 @@ inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgBackoffTime inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgErrorRate inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgWaitTime inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.estAvailBW -// -- 802.11 Block Ack: multiTid/compressedBitmap are the pair of bits that select -// the concrete Basic/Compressed/MultiTid variant, so each concrete class pins them; -// the Starting Sequence Control fragment-number subfield is always 0 (802.11 does -// not fragment the frames a Block Ack covers), and ADDBA's blockAckAction is the -// action code its own class fixes +// The concrete Block Ack classes fix their Type bits. These producers use fragmentNumber = 0. +// Compressed ACK-policy and reserved fields have no field-preserving wire contract. +// The serializer writes zero for these fields and ignores received reserved bits. +// ADDBA fixes its action code through its concrete class. inet::ieee80211::Ieee80211AddbaRequest::_fragmentNumber inet::ieee80211::Ieee80211AddbaRequest::blockAckAction inet::ieee80211::Ieee80211BasicBlockAck::compressedBitmap @@ -434,11 +433,15 @@ inet::ieee80211::Ieee80211BasicBlockAckReq::compressedBitmap inet::ieee80211::Ieee80211BasicBlockAckReq::fragmentNumber inet::ieee80211::Ieee80211BasicBlockAckReq::multiTid inet::ieee80211::Ieee80211CompressedBlockAck::compressedBitmap +inet::ieee80211::Ieee80211CompressedBlockAck::blockAckPolicy inet::ieee80211::Ieee80211CompressedBlockAck::fragmentNumber inet::ieee80211::Ieee80211CompressedBlockAck::multiTid +inet::ieee80211::Ieee80211CompressedBlockAck::reserved +inet::ieee80211::Ieee80211CompressedBlockAckReq::barAckPolicy inet::ieee80211::Ieee80211CompressedBlockAckReq::compressedBitmap inet::ieee80211::Ieee80211CompressedBlockAckReq::fragmentNumber inet::ieee80211::Ieee80211CompressedBlockAckReq::multiTid +inet::ieee80211::Ieee80211CompressedBlockAckReq::reserved // -- MRP TLV length bytes: the serializer derives them from what it writes, so // the model field is not the value on the wire inet::MrpEnd::valueLength diff --git a/tests/unit/Ieee80211BlockAckWire_1.test b/tests/unit/Ieee80211BlockAckWire_1.test index 0cd32ee3a46..41c8138da32 100644 --- a/tests/unit/Ieee80211BlockAckWire_1.test +++ b/tests/unit/Ieee80211BlockAckWire_1.test @@ -1,36 +1,230 @@ %description: -Check Basic and Compressed bitmap values after independent byte decode. -The decoder must preserve the same bitmap contents without unowned temporary objects. -Type validation remains outside this prerequisite. +Check Basic Type 0 and Compressed Type 2 BAR and Block Ack against independent octets. +Check reserved bits, unsupported Types, malformed fragment subfields, and fixed lengths. +The wire oracle follows the C1 contract in the Compressed Block Ack replacement plan. +Basic fields preserve the retained Type 0 compatibility contract. +Compressed uses the non-HE, 64-position format from IEEE Std 802.11-2024, 9.3.1.7.2 and 9.3.1.8.2. +This test does not establish complete exchanges or response airtime. %includes: #include "inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" +%global: using namespace inet; using namespace inet::ieee80211; -%activity: -for (bool compressed : {false, true}) { - std::vector bytes(compressed ? 28 : 148, 0); - bytes[0] = 0x94; - bytes[16] = compressed ? 4 : 0; - bytes[20] = 1; +class BlockAckWireSerializer : public Ieee80211MacHeaderSerializer +{ + public: + using Ieee80211MacHeaderSerializer::serializeFields; + using Ieee80211MacHeaderSerializer::deserializeFields; +}; + +static std::vector encode(const Ptr& header) +{ + BlockAckWireSerializer serializer; + MemoryOutputStream stream; + serializer.serializeFields(stream, header); + return stream.getData(); +} + +static Ptr decode(const std::vector& bytes) +{ MemoryInputStream stream(bytes); - auto decoded = Chunk::deserialize(stream, typeid(Ieee80211MacHeader)); - if (compressed) { - auto response = dynamicPtrCast(decoded); - ASSERT(response && response->getBlockAckBitmap().getSize() == 64); - ASSERT(response->getBlockAckBitmap().getBit(0)); - ASSERT(!response->getBlockAckBitmap().getBit(63)); + return Chunk::deserialize(stream, typeid(Ieee80211MacHeader)); +} + +static void requireWriteFailure(const Ptr& header) +{ + bool rejected = false; + try { encode(header); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); +} + +static void addresses(const Ptr& header) +{ + header->setReceiverAddress(MacAddress("02:11:22:33:44:55")); + header->setTransmitterAddress(MacAddress("06:aa:bb:cc:dd:ee")); + header->setDurationField(SimTime(0x1234, SIMTIME_US)); +} + +// Each vector is independent of the serializer output. +static std::vector golden(bool response, bool compressed) +{ + std::vector bytes = { + uint8_t(response ? 0x94 : 0x84), 0x00, 0x34, 0x12, + 0x02, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x06, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, + uint8_t(compressed ? 0x04 : 0x21), 0x50, 0xe0, 0xff + }; + if (response) { + bytes.resize(compressed ? 28 : 148, 0); + // SSN 4094: positions 0, 1, 2, and 63 represent 4094, 4095, 0, and 61. + if (compressed) { + bytes[20] = 0x07; + bytes[27] = 0x80; + } + else { + // Basic position 0 reports fragments 0 and 15; position 63 reports fragment 8. + bytes[20] = 0x01; + bytes[21] = 0x80; + bytes[147] = 0x01; + } + } + return bytes; +} + +%activity: +OriginatorBlockAckProcedure procedure; +auto built = dynamicPtrCast( + procedure.buildCompressedBlockAckReqFrame(MacAddress("02:11:22:33:44:55"), 5, SequenceNumberCyclic(4094))); +ASSERT(built && built->getReceiverAddress() == MacAddress("02:11:22:33:44:55")); +ASSERT(built->getTidInfo() == 5 && built->getStartingSequenceNumber().get() == 4094); +ASSERT(built->getFragmentNumber() == 0 && built->getChunkLength() == B(20)); +addresses(built); +ASSERT(encode(built) == golden(false, true)); + +for (bool response : {false, true}) { + for (bool compressed : {false, true}) { + Ptr header; + if (response) { + if (compressed) { + auto ba = makeShared(); + ba->setBlockAckPolicy(true); + ba->setReserved(0x1ff); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + ba->setBlockAckBitmap(BitVector(std::vector{7, 0, 0, 0, 0, 0, 0, 0x80})); + ASSERT(ba->getChunkLength() == B(28)); + header = ba; + } + else { + auto ba = makeShared(); + ba->setBlockAckPolicy(true); + ba->setReserved(4); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + for (int i = 0; i < 64; i++) + ba->setBlockAckBitmap(i, BitVector(std::vector{uint8_t(i == 0), uint8_t(i == 0 ? 0x80 : i == 63 ? 1 : 0)})); + ASSERT(ba->getChunkLength() == B(148)); + header = ba; + } + } + else { + if (compressed) { + auto bar = makeShared(); + bar->setBarAckPolicy(true); + bar->setReserved(0x1ff); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + header = bar; + } + else { + auto bar = makeShared(); + bar->setBarAckPolicy(true); + bar->setReserved(4); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + header = bar; + } + } + addresses(header); + auto expected = golden(response, compressed); + ASSERT(encode(header) == expected); + ASSERT(header->getChunkLength() == B(expected.size())); + if (response) { + auto fcs = makeShared(); + ASSERT(header->getChunkLength() + fcs->getChunkLength() == B(compressed ? 32 : 152)); + } + auto result = decode(expected); + ASSERT(result->isCorrect()); + ASSERT(result->isComplete()); + ASSERT(encode(result) == expected); + if (response) { + if (compressed) { + auto ba = dynamicPtrCast(result); + ASSERT(ba && ba->getTidInfo() == 5 && ba->getStartingSequenceNumber().get() == 4094); + ASSERT(ba->getBlockAckBitmap().getSize() == 64); + ASSERT(ba->getBlockAckBitmap().getBit(0) && ba->getBlockAckBitmap().getBit(2)); + ASSERT(ba->getBlockAckBitmap().getBit(63) && !ba->getBlockAckBitmap().getBit(62)); + } + else { + auto ba = dynamicPtrCast(result); + ASSERT(ba && ba->getReserved() == 4 && ba->getBlockAckPolicy()); + ASSERT(ba->getBlockAckBitmap(0).getBit(15)); + ASSERT(ba->getBlockAckBitmap(63).getBit(8)); + } + } + else { + if (compressed) { + auto bar = dynamicPtrCast(result); + ASSERT(bar && bar->getTidInfo() == 5 && bar->getStartingSequenceNumber().get() == 4094); + } + else { + auto bar = dynamicPtrCast(result); + ASSERT(bar && bar->getReserved() == 4 && bar->getBarAckPolicy()); + } + } + if (compressed) { + auto reserved = expected; + reserved[16] |= 0xe1; + reserved[17] |= 0x0f; + auto ignored = decode(reserved); + ASSERT(ignored->isCorrect()); + ASSERT(encode(ignored) == expected); + if (response) { + auto ba = dynamicPtrCast(ignored); + ASSERT(ba && !ba->getBlockAckPolicy() && ba->getReserved() == 0); + } + else { + auto bar = dynamicPtrCast(ignored); + ASSERT(bar && !bar->getBarAckPolicy() && bar->getReserved() == 0); + } + auto malformed = expected; + malformed[18] |= 1; + ASSERT(!decode(malformed)->isCorrect()); + if (response) + dynamicPtrCast(header)->setFragmentNumber(1); + else + dynamicPtrCast(header)->setFragmentNumber(1); + requireWriteFailure(header); + } + else { + for (int reserved : {1, 2, 3}) { + if (response) + dynamicPtrCast(header)->setReserved(reserved); + else + dynamicPtrCast(header)->setReserved(reserved); + requireWriteFailure(header); + } + } + for (size_t length : {size_t(17), expected.size() - 1}) { + auto shortFrame = expected; + shortFrame.resize(length); + ASSERT(!decode(shortFrame)->isComplete()); + } } - else { - auto response = dynamicPtrCast(decoded); - ASSERT(response && response->getBlockAckBitmapArraySize() == 64); - ASSERT(response->getBlockAckBitmap(0).getBit(0)); - ASSERT(!response->getBlockAckBitmap(63).getBit(15)); + for (int type = 0; type < 16; type++) { + if (type == 0 || type == 2) + continue; + auto unsupported = golden(response, true); + unsupported[16] = type << 1; + auto result = decode(unsupported); + ASSERT(!result->isCorrect()); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); } } -EV << "Bitmap decode checks passed.\n"; +for (int bits : {0, 63, 65}) { + auto ba = makeShared(); + ba->setBlockAckBitmap(bits == 0 ? BitVector() : BitVector(0u, bits)); + requireWriteFailure(ba); +} +EV << "Block Ack wire checks passed\n"; %contains: stdout -Bitmap decode checks passed. +Block Ack wire checks passed From 8522be2b8dbf7fb05c39e15cd75c24e1b57ff447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:50:08 +0200 Subject: [PATCH 12/16] ieee80211: add: prepare QoS DATA without queue extraction Before this commit, QoS data services must extract source frames before they can create an aggregate. A caller cannot inspect that aggregate before queue removal and sequence assignment. After this commit, the service creates an owned preview while original MSDUs remain in their queue. Preparation copies the next sequence value without consumption. Successful commitment validates source identity, eligibility, lifetime, aggregation permission, and sequence state. It transfers the same preview and assigns its sequence number. Refusal preserves the preview and all source frames. Ordinary commitment uses normal dequeue for the head or selected extraction for a later eligible frame. The service copies completed region metadata into the preview before source deletion. Queue service effects occur once. Aggregation retains its existing source-removal behavior. For example, ordinary DATA that waits 2 ms retains 2 ms of queueing time and one queue departure after commitment. Direct service tests cover head and non-head commitment, refusal, stale state, and cancellation. Existing extraction remains available. Later runtime activation uses this contract to admit the complete DATA exchange before destructive preparation. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- .../ieee80211-reversible-data-preparation.rst | 4 + .../ieee80211-reversible-data-preparation.txt | 5 + .../contract/IOriginatorQosMacDataService.h | 20 ++ .../originator/OriginatorQosMacDataService.cc | 156 ++++++++++ .../originator/OriginatorQosMacDataService.h | 3 + tests/module/Ieee80211PreparedQosData_1.test | 294 ++++++++++++++++++ 6 files changed, 482 insertions(+) create mode 100644 doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst create mode 100644 doc/whatsnew/incompatible/ieee80211-reversible-data-preparation.txt create mode 100644 tests/module/Ieee80211PreparedQosData_1.test diff --git a/doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst b/doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst new file mode 100644 index 00000000000..cc75b3d75f4 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst @@ -0,0 +1,4 @@ +IEEE 802.11 reversible DATA preparation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Custom QoS data services must implement the reversible preparation operations in ``IOriginatorQosMacDataService``. ``prepareFrame()`` creates an owned preview and retains original MSDUs in their queue. ``isPreparationValid()`` checks current source identity, eligibility, lifetime, sequence state, and aggregation permission. ``commitPreparedFrame()`` transfers the same preview pointer only after successful validation. Failure must preserve the preview and all original queue frames. diff --git a/doc/whatsnew/incompatible/ieee80211-reversible-data-preparation.txt b/doc/whatsnew/incompatible/ieee80211-reversible-data-preparation.txt new file mode 100644 index 00000000000..4dd8b9db242 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-reversible-data-preparation.txt @@ -0,0 +1,5 @@ +IEEE 802.11 reversible DATA preparation + +QoS data services now supply an owned DATA preview before queue extraction or sequence assignment. A successful commit transfers that same preview to the caller. A refused commit preserves the preview and original queue frames. Custom services must implement the preparation contract in the migration guide. The existing extraction path remains available. + +Commitment of ordinary DATA now preserves normal queue service effects. Queue output statistics count the departure, and the retained preview includes elapsed queueing time and queue events. This also applies when a deferred head causes selection of a later eligible frame. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h index e66fa555666..04f0ecda71b 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h @@ -7,12 +7,27 @@ #ifndef __INET_IORIGINATORQOSMACDATASERVICE_H #define __INET_IORIGINATORQOSMACDATASERVICE_H +#include + #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" namespace inet { namespace ieee80211 { +/** Owns a reversible DATA preview. Source packet identities refer to frames still owned by the queue. */ +struct INET_API PreparedQosData : public cObject +{ + std::unique_ptr packet; + queueing::IPacketQueue *queue = nullptr; + std::vector sourceIds; + bool aggregate = false; + + ~PreparedQosData() override; + void setPacket(Packet *value); + Packet *releasePacket(); +}; + class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService { public: @@ -20,6 +35,11 @@ class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const = 0; virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const = 0; virtual bool hasAMsduAggregation() const = 0; + // These QoS DATA methods separate reversible preparation from extraction and sequence assignment. + virtual std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) = 0; + virtual bool isPreparationValid(const PreparedQosData& preparation, simtime_t start) const = 0; + // Success transfers the preview to the caller. Failure retains the preview and all queued sources. + virtual Packet *commitPreparedFrame(PreparedQosData& preparation) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index 76d15004acb..11ec3cb722d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -22,6 +22,27 @@ namespace ieee80211 { Define_Module(OriginatorQosMacDataService); +PreparedQosData::~PreparedQosData() +{ + if (packet) + drop(packet.get()); +} + +void PreparedQosData::setPacket(Packet *value) +{ + if (packet) + drop(packet.get()); + packet.reset(value); + if (packet) + take(packet.get()); +} + +Packet *PreparedQosData::releasePacket() +{ + drop(packet.get()); + return packet.release(); +} + void OriginatorQosMacDataService::initialize() { aMsduAggregationPolicy = dynamic_cast(getSubmodule("msduAggregationPolicy")); @@ -35,6 +56,141 @@ void OriginatorQosMacDataService::initialize() fragmentation = new Fragmentation(); } +std::unique_ptr OriginatorQosMacDataService::prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) +{ + Enter_Method("prepareFrame"); + if (expireFrames) + expireFrames(); + auto selected = peekEligibleFrame(queue); + if (!selected) + return nullptr; + auto selectedHeader = dynamicPtrCast(selected->peekAtFront()); + if (!selectedHeader || selectedHeader->getType() != ST_DATA_WITH_QOS) + return nullptr; + auto result = std::make_unique(); + result->queue = queue; + std::vector sources{selected}; + auto module = dynamic_cast(queue); + auto head = queue->canPullPacket(module ? module->gate("out") : nullptr); + if (allowAggregation && selected == head && aMsduAggregationPolicy && (!aMsduPermitted || aMsduPermitted(selected))) { + std::unique_ptr> proposed(aMsduAggregationPolicy->computeAggregateFrames(queue)); + bool valid = proposed && !proposed->empty() && proposed->front() == selected; + std::set unique; + if (proposed) { + for (auto frame : *proposed) { + bool present = false; + for (int i = 0; i < queue->getNumPackets(); i++) + present |= queue->getPacket(i) == frame; + valid &= present && unique.insert(frame).second && + (!extractionEligible || extractionEligible(frame)) && (!aMsduPermitted || aMsduPermitted(frame)); + } + } + if (valid) { + sources = *proposed; + result->aggregate = true; + } + } + for (auto source : sources) + result->sourceIds.push_back(source->getId()); + if (result->aggregate) { + std::vector duplicates; + for (auto source : sources) + duplicates.push_back(source->dup()); + result->setPacket(aMsduAggregation->aggregateFrames(&duplicates)); + } + else + result->setPacket(selected->dup()); + // The preview carries the next value without consuming the sequence counter. + auto header = result->packet->removeAtFront(); + header->setSequenceNumber(getNextSequenceNumber(header->getReceiverAddress(), header->getTid())); + result->packet->insertAtFront(header); + return result; +} + +bool OriginatorQosMacDataService::isPreparationValid(const PreparedQosData& preparation, simtime_t start) const +{ + if (!preparation.packet || !preparation.queue || preparation.sourceIds.empty()) + return false; + auto header = preparation.packet->peekAtFront(); + if (header->getSequenceNumber() != getNextSequenceNumber(header->getReceiverAddress(), header->getTid())) + return false; + for (auto id : preparation.sourceIds) { + Packet *source = nullptr; + for (int i = 0; i < preparation.queue->getNumPackets(); i++) { + auto candidate = preparation.queue->getPacket(i); + if (candidate->getId() == id) { + source = candidate; + break; + } + } + if (!source || (lifetimeHandler && lifetimeHandler->isExpired(source, start)) || + (extractionEligible && !extractionEligible(source)) || + (preparation.aggregate && aMsduPermitted && !aMsduPermitted(source))) + return false; + } + return true; +} + +Packet *OriginatorQosMacDataService::commitPreparedFrame(PreparedQosData& preparation) +{ + Enter_Method("commitPreparedFrame"); + if (!isPreparationValid(preparation, simTime())) + return nullptr; + std::vector sources; + std::vector trackedIds; + simtime_t deadline = SIMTIME_MAX; + for (auto id : preparation.sourceIds) { + for (int i = 0; i < preparation.queue->getNumPackets(); i++) { + auto source = preparation.queue->getPacket(i); + if (source->getId() == id) { + sources.push_back(source); + if (lifetimeHandler && lifetimeHandler->getDeadline(source) != SIMTIME_MAX) { + trackedIds.push_back(id); + deadline = lifetimeHandler->getDeadline(source); + } + break; + } + } + } + for (auto source : sources) { + if (preparation.aggregate) + preparation.queue->removePacket(source); + else { + auto module = dynamic_cast(preparation.queue); + auto next = preparation.queue->canPullPacket(module ? module->gate("out") : nullptr); + queueing::CPacketFilterFunction filter([source](const Packet *candidate) { return candidate == source; }); + auto extracted = source == next ? preparation.queue->dequeuePacket() : + preparation.queue->pullPacket(module ? module->gate("out") : nullptr, &filter); + ASSERT(extracted == source); + // The preview predates extraction. Preserve the queue's completed service metadata. + preparation.packet->copyRegionTags(*source, source->getFrontOffset(), + preparation.packet->getFrontOffset(), source->getDataLength()); + } + take(source); + } + if (lifetimeHandler) + lifetimeHandler->prepareFrameReplacement(trackedIds); + for (auto source : sources) + delete source; + auto packet = preparation.releasePacket(); + take(packet); + if (lifetimeHandler) { + if (preparation.aggregate) { + auto edca = check_and_cast(getParentModule()->getSubmodule("edca")); + lifetimeHandler->registerFrame(packet, edca->classifyFrame(packet->peekAtFront())); + } + else if (deadline != SIMTIME_MAX) + lifetimeHandler->registerFragments({packet}, deadline); + } + auto header = packet->removeAtFront(); + assignSequenceNumber(header); + packet->insertAtFront(header); + packet->addTagIfAbsent()->setAllocationIndex(nextDataAllocationIndex++); + if (preparation.aggregate) + emit(packetAggregatedSignal, packet); + return packet; +} + Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected) { auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 997a696d46f..5f613686c90 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -61,6 +61,9 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const override; virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const override; virtual bool hasAMsduAggregation() const override { return aMsduAggregationPolicy != nullptr; } + virtual std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) override; + virtual bool isPreparationValid(const PreparedQosData& preparation, simtime_t start) const override; + virtual Packet *commitPreparedFrame(PreparedQosData& preparation) override; virtual ~OriginatorQosMacDataService(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler, std::function expiration) { lifetimeHandler = handler; diff --git a/tests/module/Ieee80211PreparedQosData_1.test b/tests/module/Ieee80211PreparedQosData_1.test new file mode 100644 index 00000000000..0d373bd4d43 --- /dev/null +++ b/tests/module/Ieee80211PreparedQosData_1.test @@ -0,0 +1,294 @@ +%description: +Check reversible QoS DATA preparation through the production data service and PacketQueue. +Ten runs combine ordinary and aggregate previews with cancellation, commitment, stale sources, and stale sequence state. +Preparation must preserve source identities, ownership, deadlines, and sequence counters. +Commitment must return the same preview pointer and consume one sequence number and allocation index. +Ordinary commitment preserves queue service signals, elapsed queueing time, and queue events for head and selected non-head frames. +Refusal must preserve the preview and the queue state that follows the explicit invalidation. +The fixture uses no HCF runtime path. Its Edca subclass supplies only the production access-category mapping for aggregate lifetime registration. + +%extraargs: -r 0..9 + +%file: PreparedData.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/common/PacketEventTag.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/queueing/queue/PacketQueue.h" + +using namespace inet; +using namespace inet::ieee80211; + +// Aggregate commitment uses classifyFrame(). This fixture does not exercise channel access. +class PreparedEdca : public Edca { + protected: + void initialize(int) override {} +}; +Define_Module(PreparedEdca); + +class PreparedService : public OriginatorQosMacDataService { + std::unique_ptr ownedLifetime; + protected: + void initialize() override { + OriginatorQosMacDataService::initialize(); + auto lifetime = SimTime(1, SIMTIME_S); + ownedLifetime = std::make_unique(lifetime, lifetime, lifetime, lifetime); + setLifetimeHandler(ownedLifetime.get(), {}); + setEligibility([](const Packet *) { return true; }, [](Packet *) { return true; }); + } + public: + EdcaTransmitLifetimeHandler *lifetime() const { return ownedLifetime.get(); } + int64_t allocations() const { return nextDataAllocationIndex; } +}; +Define_Module(PreparedService); + +class PreparedDataTest : public cSimpleModule, public cListener { + const MacAddress peer{"02:00:00:00:00:02"}; + PreparedService *service = nullptr; + queueing::PacketQueue *queue = nullptr; + queueing::PacketQueue *otherQueue = nullptr; + std::vector sources; + std::vector sourceIds; + std::vector sourceOwners; + std::vector deadlines; + std::unique_ptr preparation; + Packet *preview = nullptr; + int removals = 0; + int pulls = 0; + int aggregates = 0; + + bool aggregate() const { return par("aggregate") && operation() != "selectedCommit"; } + std::string operation() const { return par("operation").stdstringValue(); } + + Packet *enqueue(queueing::PacketQueue *destination, const char *name) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(peer); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(peer); + header->setTid(0); + header->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(100))); + packet->insertAtBack(makeShared()); + auto time = packet->addRegionTag(header->getChunkLength(), B(100)); + time->setPacketTotalTimesArraySize(1); + time->setPacketTotalTimes(0, SIMTIME_ZERO); + packet->addRegionTag(header->getChunkLength(), B(100)); + packet->setArrivalTime(simTime()); + destination->enqueuePacket(packet); + service->lifetime()->registerFrame(packet, AC_BE); + return packet; + } + + void assertSources(int first, int expectedSequence = 0, int expectedAllocations = 0) const { + ASSERT(queue->getNumPackets() == int(sources.size()) - first); + ASSERT(service->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(expectedSequence)); + ASSERT(service->allocations() == expectedAllocations); + int index = 0; + for (int i = 0; i < int(sources.size()); i++) { + if (first && (operation() == "selectedCommit" ? i == 1 : i < first)) + continue; + auto packet = queue->getPacket(index++); + ASSERT(packet == sources[i] && packet->getId() == sourceIds[i]); + ASSERT(packet->getOwner() == sourceOwners[i]); + ASSERT(packet->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(!packet->findTag()); + ASSERT(service->lifetime()->findFrame(sourceIds[i]) == packet); + ASSERT(service->lifetime()->getDeadline(packet) == deadlines[i]); + ASSERT(packet->findRegionTag(packet->peekAtFront()->getChunkLength(), B(100))->getPacketTotalTimes(0) == SIMTIME_ZERO); + } + } + + void deleteCommitted(Packet *packet) { + service->lifetime()->forgetFrame(packet->getId()); + take(packet); + delete packet; + } + + void cancelPreview() { + ASSERT(preparation->packet.get() == preview && preview->getOwner() == preparation.get()); + auto live = cOwnedObject::getLiveObjectCount(); + preparation.reset(); + ASSERT(cOwnedObject::getLiveObjectCount() == live - 1); + preview = nullptr; + } + + void prepare() { + for (const char *name : {"first-msdu", "second-msdu"}) { + auto packet = enqueue(queue, name); + sources.push_back(packet); + sourceIds.push_back(packet->getId()); + sourceOwners.push_back(packet->getOwner()); + deadlines.push_back(service->lifetime()->getDeadline(packet)); + } + if (operation() == "selectedCommit") + service->setEligibility([](const Packet *packet) { return !packet->isName("first-msdu"); }, [](Packet *) { return true; }); + preparation = service->prepareFrame(queue, aggregate()); + ASSERT(preparation && preparation->packet); + preview = preparation->packet.get(); + ASSERT(preview != sources[0] && preview != sources[1]); + ASSERT(preview->getOwner() == preparation.get()); + ASSERT(preparation->queue == queue && preparation->aggregate == aggregate()); + ASSERT(preparation->sourceIds.size() == (aggregate() ? 2 : 1)); + for (size_t i = 0; i < preparation->sourceIds.size(); i++) + ASSERT(preparation->sourceIds[i] == sourceIds[operation() == "selectedCommit" ? 1 : i]); + auto header = preview->peekAtFront(); + ASSERT(header->getAMsduPresent() == aggregate()); + ASSERT(header->getSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(!preview->findTag()); + ASSERT(service->lifetime()->findFrame(preview->getId()) == nullptr); + ASSERT(service->isPreparationValid(*preparation, simTime())); + ASSERT(removals == 0 && pulls == 0 && aggregates == 0); + assertSources(0); + scheduleAt(SimTime(3, SIMTIME_MS), new cMessage("check")); + } + + void check() { + assertSources(0); + ASSERT(service->isPreparationValid(*preparation, simTime())); + if (operation() == "commit" || operation() == "selectedCommit") { + auto committed = service->commitPreparedFrame(*preparation); + ASSERT(committed == preview && !preparation->packet); + ASSERT(committed->getOwner() == service); + ASSERT(committed->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(committed->getTag()->getAllocationIndex() == 0); + ASSERT(!service->isPreparationValid(*preparation, simTime())); + int consumed = aggregate() ? 2 : 1; + for (int i = 0; i < consumed; i++) + ASSERT(service->lifetime()->findFrame(sourceIds[operation() == "selectedCommit" ? 1 : i]) == nullptr); + auto expectedDeadline = aggregate() ? simTime() + SimTime(1, SIMTIME_S) : deadlines[operation() == "selectedCommit" ? 1 : 0]; + ASSERT(service->lifetime()->getDeadline(committed) == expectedDeadline); + ASSERT(removals == (aggregate() ? consumed : 0) && pulls == (aggregate() ? 0 : 1) && aggregates == (aggregate() ? 1 : 0)); + if (!aggregate()) { + auto offset = committed->peekAtFront()->getChunkLength(); + auto time = committed->findRegionTag(offset, B(100)); + ASSERT(time && time->getPacketTotalTimes(0) == SimTime(2, SIMTIME_MS)); + auto events = committed->findRegionTag(offset, B(100)); + ASSERT(events && events->getPacketEventsArraySize() == 1); + ASSERT(events->getPacketEvents(0)->getKind() == PEK_QUEUED); + ASSERT(events->getPacketEvents(0)->getPacketDuration() == SimTime(2, SIMTIME_MS)); + } + assertSources(consumed, 1, 1); + deleteCommitted(committed); + preparation.reset(); + preview = nullptr; + } + else if (operation() == "staleSource") { + auto removed = queue->getPacket(0); + queue->removePacket(removed); + service->lifetime()->forgetFrame(removed->getId()); + take(removed); + delete removed; + ASSERT(!service->isPreparationValid(*preparation, simTime())); + ASSERT(service->commitPreparedFrame(*preparation) == nullptr); + assertSources(1); + ASSERT(removals == 1 && pulls == 0 && aggregates == 0); + cancelPreview(); + assertSources(1); + } + else if (operation() == "staleSequence") { + enqueue(otherQueue, "independent-msdu"); + auto other = service->prepareFrame(otherQueue, false); + ASSERT(other && service->isPreparationValid(*other, simTime())); + auto otherPreview = other->packet.get(); + auto committed = service->commitPreparedFrame(*other); + ASSERT(committed == otherPreview && otherQueue->isEmpty()); + ASSERT(!service->isPreparationValid(*preparation, simTime())); + ASSERT(service->commitPreparedFrame(*preparation) == nullptr); + assertSources(0, 1, 1); + ASSERT(removals == 0 && pulls == 0 && aggregates == 0); + deleteCommitted(committed); + cancelPreview(); + assertSources(0, 1, 1); + } + else { + ASSERT(operation() == "cancel"); + cancelPreview(); + assertSources(0); + ASSERT(removals == 0 && pulls == 0 && aggregates == 0); + } + while (!queue->isEmpty()) { + auto packet = queue->getPacket(0); + queue->removePacket(packet); + deleteCommitted(packet); + } + ASSERT(otherQueue->isEmpty() && service->lifetime()->getNumFrames() == 0); + std::cout << "Prepared QoS DATA assertions passed: aggregate=" << aggregate() + << " operation=" << operation() << ".\n"; + endSimulation(); + } + + protected: + void initialize() override { + service = check_and_cast(getModuleByPath("^.service")); + queue = check_and_cast(getModuleByPath("^.queue")); + otherQueue = check_and_cast(getModuleByPath("^.otherQueue")); + queue->subscribe(packetRemovedSignal, this); + queue->subscribe(packetPulledSignal, this); + service->subscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("prepare")); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { + if (signal == packetRemovedSignal) removals++; + if (signal == packetPulledSignal) pulls++; + if (signal == IOriginatorMacDataService::packetAggregatedSignal) aggregates++; + } + void handleMessage(cMessage *message) override { + bool first = message->isName("prepare"); + delete message; + if (first) + prepare(); + else + check(); + } +}; +Define_Module(PreparedDataTest); + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.originator.OriginatorQosMacDataService; +import inet.queueing.queue.PacketQueue; + +simple PreparedEdca extends SimpleModule { parameters: @class(::PreparedEdca); } +module PreparedService extends OriginatorQosMacDataService { parameters: @class(::PreparedService); } +simple PreparedDataTest { + parameters: + bool aggregate; + string operation; + @class(::PreparedDataTest); +} +network PreparedDataNetwork { + submodules: + edca: PreparedEdca; + service: PreparedService; + queue: PacketQueue; + otherQueue: PacketQueue; + test: PreparedDataTest; + connections allowunconnected: +} + +%inifile: omnetpp.ini +[General] +network = PreparedDataNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.aggregate = ${aggregate=false,true} +*.test.operation = ${operation="cancel","commit","staleSource","staleSequence","selectedCommit"} +*.service.msduAggregationPolicy.subframeNumThreshold = 2 +*.service.msduAggregationPolicy.maxAMsduSize = 1000 +*.service.mpduAggregationPolicy.typename = "" +*.service.fragmentationPolicy.typename = "" + +%contains: stdout +Prepared QoS DATA assertions passed: From 8d9314d1835c70b0268b9d895e78835fbae56d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:50:34 +0200 Subject: [PATCH 13/16] ieee80211: add: retain explicit Compressed receipt state Before this commit, recipient receipt state uses the Basic fragment bitmap. Delivery cleanup cannot supply the independent complete-MPDU receipt evidence that a Compressed response needs. After this commit, the record retains explicit complete receipts within its Compressed window. The agreement selects that record profile. The response builder copies receipt truth into a 64-bit bitmap. Positive receipts survive delivery cleanup, while absent positions remain zero. For example, a window at 4094 retains sequence 4094 after delivery and moves across sequence wrap without false positive bits. The direct record fixture checks window movement, the preceding receipt, absent positions, and response construction. HCF still refuses Compressed selection. This prerequisite activates no incomplete exchange. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew | ieee80211-compressed-blockack-replacement --- .../ieee80211-compressed-receipt-helpers.txt | 3 + .../ieee80211/mac/blockack/BlockAckRecord.cc | 44 ++++++++++++ .../ieee80211/mac/blockack/BlockAckRecord.h | 9 +++ .../mac/blockack/RecipientBlockAckAgreement.h | 8 +++ .../blockack/RecipientBlockAckProcedure.cc | 14 ++++ .../Ieee80211CompressedBlockAckRecord_1.test | 71 +++++++++++++++++++ 6 files changed, 149 insertions(+) create mode 100644 doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt create mode 100644 tests/unit/Ieee80211CompressedBlockAckRecord_1.test diff --git a/doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt b/doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt new file mode 100644 index 00000000000..65894263aca --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt @@ -0,0 +1,3 @@ +Compressed receipt helpers + +Compressed receipt helpers now retain positive receipt state independently of delivery. HCF still refuses the Compressed profile until complete exchange support exists. diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 919bd813a6f..fa285580703 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -53,6 +53,50 @@ void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) } } +void BlockAckRecord::initializeCompressedWindow(SequenceNumberCyclic start, int size) +{ + ASSERT(start.isSpecified() && size > 0 && size <= 64); + windowStart = start; + windowSize = size; + completeReceipts.clear(); +} + +void BlockAckRecord::advanceCompressedWindow(SequenceNumberCyclic start) +{ + // IEEE Std 802.11-2024, 10.25.6.3: retain explicit receipt truth at WinStartR - 1. + if (!(windowStart < start)) + return; + windowStart = start; + for (auto it = completeReceipts.begin(); it != completeReceipts.end();) { + auto distance = (*it - windowStart.get() + 4096) % 4096; + if (distance >= windowSize && distance != 4095) + it = completeReceipts.erase(it); + else + ++it; + } +} + +void BlockAckRecord::compressedFrameReceived(const Ptr& header) +{ + ASSERT(header->getFragmentNumber() == 0 && !header->getMoreFragments()); + auto sequence = header->getSequenceNumber(); + // IEEE Std 802.11-2024, 10.25.6.3: advance before duplicate removal or reorder insertion. + if (windowStart <= sequence && !(sequence < windowStart + windowSize)) + advanceCompressedWindow(sequence - windowSize + 1); + if (windowStart <= sequence && sequence < windowStart + windowSize) + completeReceipts.insert(sequence.get()); +} + +bool BlockAckRecord::hasCompleteReceipt(SequenceNumberCyclic sequenceNumber) const +{ + return completeReceipts.count(sequenceNumber.get()) != 0; +} + +bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) const +{ + return windowStart <= sequenceNumber && sequenceNumber < windowStart + windowSize && hasCompleteReceipt(sequenceNumber); +} + } /* 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..1c90bf392dc 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -25,6 +25,9 @@ class INET_API BlockAckRecord MacAddress originatorAddress = MacAddress::UNSPECIFIED_ADDRESS; Tid tid = -1; std::map acknowledgmentState; + SequenceNumberCyclic windowStart; + int windowSize = 64; // Sequence positions; the Compressed profile permits 1..64. + std::set completeReceipts; public: BlockAckRecord(MacAddress originatorAddress, Tid tid); @@ -33,6 +36,12 @@ class INET_API BlockAckRecord void blockAckPolicyFrameReceived(const Ptr& header); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); void removeAckStates(SequenceNumberCyclic sequenceNumber); + void initializeCompressedWindow(SequenceNumberCyclic start, int size); + void compressedFrameReceived(const Ptr& header); + void advanceCompressedWindow(SequenceNumberCyclic start); + bool getCompressedAckState(SequenceNumberCyclic sequenceNumber) const; + bool hasCompleteReceipt(SequenceNumberCyclic sequenceNumber) const; + SequenceNumberCyclic getWindowStart() const { return windowStart; } MacAddress getOriginatorAddress() { return originatorAddress; } Tid getTid() { return tid; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 6e9634cae6f..db10d2d489b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -9,6 +9,7 @@ #define __INET_RECIPIENTBLOCKACKAGREEMENT_H #include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" namespace inet { namespace ieee80211 { @@ -25,6 +26,7 @@ class INET_API RecipientBlockAckAgreement : public cObject simtime_t expirationTime = -1; SequenceNumberCyclic highestReceivedSequence; int highestReceivedFragment = 0; + BlockAckVariant variant = BlockAckVariant::BASIC; public: RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue); @@ -35,6 +37,12 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual BlockAckRecord *getBlockAckRecord() const { return blockAckRecord; } virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } + BlockAckVariant getVariant() const { return variant; } + void setVariant(BlockAckVariant value) { + variant = value; + if (variant == BlockAckVariant::COMPRESSED) + blockAckRecord->initializeCompressedWindow(startingSequenceNumber, bufferSize); + } void recordReceived(const Ptr& header) { auto sequence = header->getSequenceNumber(); if (!highestReceivedSequence.isSpecified() || highestReceivedSequence < sequence) { diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index ab7cb050e5f..7573552b754 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -65,6 +65,20 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr blockAck->setTidInfo(basicBlockAckReq->getTidInfo()); return blockAck; } + else if (auto compressed = dynamicPtrCast(blockAckReq)) { + ASSERT(agreement != nullptr); + auto blockAck = makeShared(); + auto record = agreement->getBlockAckRecord(); + auto start = record->getWindowStart(); + BitVector bitmap(0, 64); + for (int i = 0; i < 64; i++) + bitmap.setBit(i, record->getCompressedAckState(start + i)); + blockAck->setBlockAckBitmap(bitmap); + blockAck->setReceiverAddress(compressed->getTransmitterAddress()); + blockAck->setStartingSequenceNumber(start); + blockAck->setTidInfo(compressed->getTidInfo()); + return blockAck; + } else throw cRuntimeError("Unsupported Block Ack Request"); } diff --git a/tests/unit/Ieee80211CompressedBlockAckRecord_1.test b/tests/unit/Ieee80211CompressedBlockAckRecord_1.test new file mode 100644 index 00000000000..58daa02e92a --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAckRecord_1.test @@ -0,0 +1,71 @@ +%description: +Check explicit Compressed receipt state independently of the delivery buffer. +Absent positions remain zero after delivery, window movement, and sequence wrap. +The preceding receipt remains available for the individual-MPDU release condition. +These checks do not establish HCF interframe recognition. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" + +using namespace inet; +using namespace inet::ieee80211; + +class ReceiptProcedure : public RecipientBlockAckProcedure { + public: + using RecipientBlockAckProcedure::buildBlockAck; +}; + +%activity: +const MacAddress peer("02:00:00:00:00:02"); +for (int start : {100, 4094}) { + RecipientBlockAckAgreement agreement(peer, 5, SequenceNumberCyclic(start), 4, SIMTIME_ZERO); + agreement.setVariant(BlockAckVariant::COMPRESSED); + auto record = agreement.getBlockAckRecord(); + auto bar = makeShared(); + bar->setTransmitterAddress(peer); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(start)); + ReceiptProcedure procedure; + auto response = dynamicPtrCast(procedure.buildBlockAck(bar, &agreement)); + ASSERT(response != nullptr && response->getChunkLength() == B(28)); + ASSERT(response->getBlockAckBitmap().getSize() == 64); + for (int bit = 0; bit < 64; bit++) + ASSERT(!response->getBlockAckBitmap().getBit(bit)); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setTransmitterAddress(peer); + data->setTid(5); + data->setAckPolicy(NORMAL_ACK); + data->setSequenceNumber(SequenceNumberCyclic(start)); + record->compressedFrameReceived(data); + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start))); + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start) - 1)); + // Delivery and Basic compatibility cleanup must not erase Compressed receipt truth. + record->removeAckStates(SequenceNumberCyclic(start) + 1); + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start))); + data->setSequenceNumber(SequenceNumberCyclic(start) + 4); + record->compressedFrameReceived(data); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(start) + 1); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(start))); + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start))); + response = dynamicPtrCast(procedure.buildBlockAck(bar, &agreement)); + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(start) + 1); + for (int bit = 0; bit < 64; bit++) + ASSERT(response->getBlockAckBitmap().getBit(bit) == (bit == 3)); + record->advanceCompressedWindow(SequenceNumberCyclic(start) + 5); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(start) + 4)); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(start))); + record->advanceCompressedWindow(SequenceNumberCyclic(start) + 8); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(start) + 4)); + for (int bit = 0; bit < 64; bit++) + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start) + 8 + bit)); + // An old BAR must preserve the current scoreboard start. + record->advanceCompressedWindow(SequenceNumberCyclic(start)); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(start) + 8); +} +EV << "Compressed receipt state checks passed.\n"; + +%contains: stdout +Compressed receipt state checks passed. From 17220a31b93b4deded132f683d99714ce3db3125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:51:17 +0200 Subject: [PATCH 14/16] ieee80211: add: select legal Compressed control rates Before this commit, QoS rate selection supports only the Basic Block Ack response contract. Compressed exchanges need distinct non-HT BAR selection and response-duration rules. After this commit, the selector uses the applicable local, BSS, and peer rate sets for initial and later BAR. Compressed responses use their non-HT reference rate and 32-byte airtime. Invalid configured overrides fail explicitly. The new pure interface operation requires matching custom implementations. For example, two peers with different operational rates can require different later-BAR rates. The direct rate fixture checks both peers, both response directions, Basic compatibility, and invalid overrides. HCF still refuses Compressed selection. This prerequisite activates no incomplete exchange. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- .../ieee80211-compressed-control-rates.rst | 4 + ...e80211-compressed-control-rate-helpers.txt | 3 + .../mac/contract/IQosRateSelection.h | 3 + .../mac/rateselection/QosRateSelection.cc | 80 +++++++++ .../mac/rateselection/QosRateSelection.h | 2 + .../Ieee80211HcfMultipleProtection_1.test | 3 + .../Ieee80211HcfSingleProtection_1.test | 3 + .../Ieee80211SingleProtectionGroup_1.test | 1 + tests/module/Ieee80211Txnav_1.test | 1 + tests/module/Ieee80211VhtHtSupport_1.test | 3 + .../Ieee80211CompressedBlockAckRate_1.test | 169 ++++++++++++++++++ tests/unit/Ieee80211ResponseTimeout_1.test | 1 + 12 files changed, 273 insertions(+) create mode 100644 doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst create mode 100644 doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt create mode 100644 tests/unit/Ieee80211CompressedBlockAckRate_1.test diff --git a/doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst b/doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst new file mode 100644 index 00000000000..8852fbf1ffd --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst @@ -0,0 +1,4 @@ +IEEE 802.11 Compressed control rates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Custom QoS rate selectors must implement ``computeCompressedBlockAckReqMode(packet, header, startsTxop)``. This operation prepares legal non-HT BAR rate selection from the supplied TXOP position and applicable BSS or peer rates. HCF retains its Compressed-profile rejection in this prerequisite. Runtime activation will supply the request's actual TXOP position. Existing Basic methods retain their contracts. diff --git a/doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt b/doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt new file mode 100644 index 00000000000..34a489d57cf --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt @@ -0,0 +1,3 @@ +Compressed control-rate helpers + +QoS rate selection now supplies non-HT Compressed BAR and response modes. HCF still refuses the Compressed profile until complete exchange support exists. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h index 34a38ac1d9a..10c35f613a0 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h @@ -32,6 +32,9 @@ class INET_API IQosRateSelection virtual const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame, ResponseQueryRole role) = 0; virtual const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& dataOrMgmtHeader, ResponseQueryRole role) = 0; virtual const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq, ResponseQueryRole role) = 0; + // HCF derives startsTxop from the current frame sequence before this query. + virtual const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, + const Ptr& blockAckReq, bool startsTxop) = 0; virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 82db4bbd06c..5c65ba9d116 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -144,10 +144,90 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet auto mode = responseBlockAckFrameMode ? responseBlockAckFrameMode : getMode(packet, blockAckReq); return getPeerCompatibleResponseMode(blockAckReq, mode, role); } + else if (dynamicPtrCast(blockAckReq)) { + auto received = getMode(packet, blockAckReq); + auto referenceRate = received->getNonHtReferenceRate(); + auto receivedClass = received->getModulationClass(); + bool highThroughput = receivedClass == IIeee80211Mode::ModulationClass::HT || receivedClass == IIeee80211Mode::ModulationClass::VHT; + if (!std::isfinite(referenceRate.get())) + throw cRuntimeError("Compressed Block Ack has no non-HT reference rate for the BAR mode"); + const Ieee80211RateSet *basic = mib ? &mib->getBssRateSet().basic : nullptr; + auto matchesClass = [received, receivedClass, highThroughput](const IIeee80211Mode *candidate) { + auto modulation = candidate->getModulationClass(); + return highThroughput ? modulation == IIeee80211Mode::ModulationClass::OFDM || modulation == IIeee80211Mode::ModulationClass::ERP_OFDM : + modulation == receivedClass && candidate->getLegacyPreambleType() == received->getLegacyPreambleType(); + }; + const IIeee80211Mode *primary = nullptr; + // IEEE Std 802.11-2024, 10.6.6.5.2: prefer a qualifying BSS basic rate, then a mandatory rate. + for (bool useBasic : {true, false}) { + if (useBasic && (!basic || !basic->known)) + continue; // INET fallback for unknown BSS state uses the mandatory rate set. + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto candidate = modeSet->getMode(i); + auto rate = candidate->getDataMode()->getNetBitrate(); + bool eligible = useBasic ? basic->legacyRates.count(rate) != 0 : modeSet->isMandatory(i); + if (eligible && matchesClass(candidate) && rate <= referenceRate && + (!primary || rate > primary->getDataMode()->getNetBitrate())) + primary = candidate; + } + if (primary) + break; + } + if (!primary) + throw cRuntimeError("Compressed Block Ack has no legal non-HT response mode"); + if (!responseBlockAckFrameMode || responseBlockAckFrameMode == primary) + return primary; + auto alternate = responseBlockAckFrameMode; + auto rate = alternate->getDataMode()->getNetBitrate(); + bool basicOrMandatory = (basic && basic->known && basic->legacyRates.count(rate)) || modeSet->getIsMandatory(alternate); + // IEEE Std 802.11-2024, 10.6.6.5.4: an alternate preserves modulation and actual response duration. + if (!matchesClass(alternate) || alternate->getModulationClass() != primary->getModulationClass() || !basicOrMandatory || + alternate->getDuration(LENGTH_COMPRESSED_BLOCKACK) != primary->getDuration(LENGTH_COMPRESSED_BLOCKACK)) + throw cRuntimeError("responseBlockAckFrameBitrate is not a legal alternate Compressed Block Ack response rate"); + return alternate; + } else throw cRuntimeError("Unknown BlockAckReq frame type"); } +const IIeee80211Mode *QosRateSelection::computeCompressedBlockAckReqMode(Packet *packet, + const Ptr& blockAckReq, bool startsTxop) +{ + const Ieee80211RateSet *local = mib ? &mib->getLocalRateSet().operational : nullptr; + const Ieee80211RateSet *basic = mib ? &mib->getBssRateSet().basic : nullptr; + auto peerState = mib ? mib->findPeerRateSet(blockAckReq->getReceiverAddress()) : nullptr; + const Ieee80211RateSet *peer = peerState ? &peerState->operational : nullptr; + auto eligible = [&](const IIeee80211Mode *candidate) { + auto modulation = candidate->getModulationClass(); + bool legacy = modulation == IIeee80211Mode::ModulationClass::DSSS_HRDSSS || + modulation == IIeee80211Mode::ModulationClass::OFDM || modulation == IIeee80211Mode::ModulationClass::ERP_OFDM; + auto rate = candidate->getDataMode()->getNetBitrate(); + if (!legacy || (local && local->known && !local->legacyRates.count(rate))) + return false; + // IEEE Std 802.11-2024, 10.6.6.2 and 10.6.6.4: initial BAR and later BAR use different sets. + if (!startsTxop && peer && peer->known) + return peer->legacyRates.count(rate) != 0; + if (basic && basic->known && !basic->legacyRates.empty()) + return basic->legacyRates.count(rate) != 0; + // INET uses mandatory rates when BSS rate state is unknown. + return modeSet->getIsMandatory(candidate); + }; + if (controlFrameMode) { + if (!eligible(controlFrameMode)) + throw cRuntimeError("controlFrameBitrate is not a legal non-HT Compressed BAR rate for this TXOP position"); + return controlFrameMode; + } + const IIeee80211Mode *selected = nullptr; + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto candidate = modeSet->getMode(i); + if (eligible(candidate) && (!selected || candidate->getDataMode()->getNetBitrate() > selected->getDataMode()->getNetBitrate())) + selected = candidate; + } + if (!selected) + throw cRuntimeError("Compressed BAR has no legal local non-HT mode"); + return selected; +} + const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 5bc4647a89e..c0ecc61f8a1 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -84,6 +84,8 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame, ResponseQueryRole role) override; virtual const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& dataOrMgmtHeader, ResponseQueryRole role) override; virtual const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq, ResponseQueryRole role) override; + virtual const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, + const Ptr& blockAckReq, bool startsTxop) override; virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) override; diff --git a/tests/module/Ieee80211HcfMultipleProtection_1.test b/tests/module/Ieee80211HcfMultipleProtection_1.test index ea7991de3ee..852333eb985 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_1.test +++ b/tests/module/Ieee80211HcfMultipleProtection_1.test @@ -109,6 +109,9 @@ class ProtectionRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } }; class ProtectionHcf : public Hcf { diff --git a/tests/module/Ieee80211HcfSingleProtection_1.test b/tests/module/Ieee80211HcfSingleProtection_1.test index bcf69e606b9..1ef31e67f3d 100644 --- a/tests/module/Ieee80211HcfSingleProtection_1.test +++ b/tests/module/Ieee80211HcfSingleProtection_1.test @@ -91,6 +91,9 @@ class SingleRateSelection : public IQosRateSelection { const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } }; class SingleRateControl : public IRateControl { public: diff --git a/tests/module/Ieee80211SingleProtectionGroup_1.test b/tests/module/Ieee80211SingleProtectionGroup_1.test index c5ff47f33c0..0bb495f62cf 100644 --- a/tests/module/Ieee80211SingleProtectionGroup_1.test +++ b/tests/module/Ieee80211SingleProtectionGroup_1.test @@ -28,6 +28,7 @@ class GroupRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txop) override { return delegate->computeMode(packet, header, txop); } const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseCtsFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } }; class GroupMechanism : public SingleProtectionMechanism { public: diff --git a/tests/module/Ieee80211Txnav_1.test b/tests/module/Ieee80211Txnav_1.test index 452f1bc2091..0e23937594f 100644 --- a/tests/module/Ieee80211Txnav_1.test +++ b/tests/module/Ieee80211Txnav_1.test @@ -32,6 +32,7 @@ class TxnavSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseCtsFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseAckFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } }; class TxnavRx : public Rx { public: diff --git a/tests/module/Ieee80211VhtHtSupport_1.test b/tests/module/Ieee80211VhtHtSupport_1.test index 72514a2b82b..f9b9501aada 100644 --- a/tests/module/Ieee80211VhtHtSupport_1.test +++ b/tests/module/Ieee80211VhtHtSupport_1.test @@ -68,6 +68,9 @@ class VhtHtSelection : public IQosRateSelection { const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } }; class VhtHtRx : public Rx { public: diff --git a/tests/unit/Ieee80211CompressedBlockAckRate_1.test b/tests/unit/Ieee80211CompressedBlockAckRate_1.test new file mode 100644 index 00000000000..1aa02ea3a16 --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAckRate_1.test @@ -0,0 +1,169 @@ +%description: +Check Compressed response rates through the production QoS rate selector. +Use independent MIB rate sets for two peers and both response directions. +Retain the Basic response contract and reject an invalid Compressed override. + +%includes: +#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 Ieee80211CompressedBlockAckRate_1 { +class CompressedRateSelection : public QosRateSelection { + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LOCAL) + mib.reference(this, "mibModule", true); + } + public: + void configure(const Ieee80211ModeSet *catalog, const IIeee80211Mode *response) { + modeSet = catalog; + responseBlockAckFrameMode = response; + } + void controlOverride(const IIeee80211Mode *mode) { controlFrameMode = mode; } +}; +Define_Module(CompressedRateSelection); + +static Ieee80211RateSetState rates(std::initializer_list basic) { + Ieee80211RateSetState state; + state.supported.known = state.operational.known = state.basic.known = true; + for (int rate : {6, 9, 12, 18, 24, 36, 48, 54}) + state.supported.legacyRates.insert(Mbps(rate)); + state.operational = state.supported; + for (int rate : basic) + state.basic.legacyRates.insert(Mbps(rate)); + return state; +} +} + +%file: TestNetwork.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mib.Ieee80211Mib; +simple CompressedRateSelection extends SimpleModule { + parameters: + @class(CompressedRateSelection); + string mibModule; +} +network TestNetwork { + submodules: + test: Test; + mib: Ieee80211Mib; + selector: CompressedRateSelection { parameters: mibModule = "^.mib"; } +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +record-vector-results = false +record-scalar-results = false + +%activity: +using namespace Ieee80211CompressedBlockAckRate_1; +auto mib = check_and_cast(getModuleByPath("mib")); +auto selector = check_and_cast(getModuleByPath("selector")); +const MacAddress a("02:00:00:00:00:01"), b("02:00:00:00:00:02"); +for (const char *name : {"n(mixed-2.4Ghz)", "ac"}) { + auto catalog = Ieee80211ModeSet::getModeSet(name); + auto mode = [&](int rate) { return catalog->getMode(Mbps(rate)); }; + auto inputCatalog = Ieee80211ModeSet::getModeSet(name[0] == 'a' ? "a" : "g(mixed)"); + auto fastInput = inputCatalog->getMode(Mbps(54)); + mib->setLocalRateSet(rates({6, 12, 24})); + mib->setBssRateSet(rates({6, 12, 24})); + selector->configure(catalog, nullptr); + auto bar = makeShared(); + bar->setReceiverAddress(b); + bar->setTransmitterAddress(a); + auto check = [&](const IIeee80211Mode *input, int expected) { + Packet predict("predict"), respond("respond"); + predict.addTag()->setMode(input); + respond.addTag()->setMode(input); + auto predicted = selector->computeResponseBlockAckFrameMode(&predict, bar, ResponseQueryRole::PREDICT); + auto response = selector->computeResponseBlockAckFrameMode(&respond, bar, ResponseQueryRole::RESPOND); + ASSERT(predicted == mode(expected)); + ASSERT(response == predicted); + ASSERT(response->getHtMcsIndex() < 0); + ASSERT(response->getDuration(B(32)) < response->getDuration(LENGTH_BASIC_BLOCKACK)); + }; + check(fastInput, 24); + check(mode(6), 6); + mib->setBssRateSet(rates({48})); + check(mode(24), 24); + mib->clearBssRateSet(); + check(fastInput, 24); + mib->setBssRateSet(rates({})); + check(fastInput, 24); + mib->setBssRateSet(rates({6, 12, 24})); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto ht = catalog->getMode(i); + auto family = ht->getModulationClass(); + if (family != IIeee80211Mode::ModulationClass::HT && family != IIeee80211Mode::ModulationClass::VHT) + continue; + auto reference = ht->getNonHtReferenceRate(); + int expected = reference >= Mbps(24) ? 24 : reference >= Mbps(12) ? 12 : 6; + check(ht, expected); + } + mib->setBssRateSet(rates({48})); + selector->configure(catalog, mode(6)); + bool refused = false; + try { check(fastInput, 6); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + selector->configure(catalog, nullptr); + auto basic = makeShared(); + Packet packet("basic"); + packet.addTag()->setMode(mode(24)); + ASSERT(selector->computeResponseBlockAckFrameMode(&packet, basic, ResponseQueryRole::RESPOND) == mode(24)); + auto bss = rates({6, 12, 24}); + auto peer = rates({6}); + peer.operational.legacyRates = {Mbps(6), Mbps(12)}; + mib->installBssAndPeerRateSets(bss, b, peer); + Packet originated("originated"); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, false) == mode(12)); + mib->removePeerRateSet(b); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, false) == mode(24)); + mib->setBssRateSet(rates({})); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + mib->clearBssRateSet(); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + selector->controlOverride(mode(6)); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(6)); + mib->setBssRateSet(rates({6})); + selector->controlOverride(mode(24)); + refused = false; + try { selector->computeCompressedBlockAckReqMode(&originated, bar, true); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto ht = catalog->getMode(i); + if (ht->getModulationClass() != IIeee80211Mode::ModulationClass::HT) + continue; + selector->controlOverride(ht); + refused = false; + try { selector->computeCompressedBlockAckReqMode(&originated, bar, true); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + break; + } + selector->controlOverride(nullptr); +} +// The full OFDM catalog supplies optional rates for the alternate-rate contract. +auto ofdm = Ieee80211ModeSet::getModeSet("a"); +auto primary = ofdm->getMode(Mbps(48)); +auto alternate = ofdm->getMode(Mbps(36)); +ASSERT(primary->getDuration(B(32)) == alternate->getDuration(B(32))); +mib->setBssRateSet(rates({36, 48})); +selector->configure(ofdm, alternate); +Packet alternatePacket("alternate"); +alternatePacket.addTag()->setMode(ofdm->getMode(Mbps(54))); +auto alternateBar = makeShared(); +ASSERT(selector->computeResponseBlockAckFrameMode(&alternatePacket, alternateBar, ResponseQueryRole::RESPOND) == alternate); +EV << "Compressed response rate checks passed.\n"; + +%contains: stdout +Compressed response rate checks passed. diff --git a/tests/unit/Ieee80211ResponseTimeout_1.test b/tests/unit/Ieee80211ResponseTimeout_1.test index dcafbffa653..c55f286bb21 100644 --- a/tests/unit/Ieee80211ResponseTimeout_1.test +++ b/tests/unit/Ieee80211ResponseTimeout_1.test @@ -49,6 +49,7 @@ class TestRateSelection : public SimpleModule, public IRateSelection, public IQo const Ptr& header) override { return responseMode; } virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) override { return responseMode; } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *, const Ptr&, bool) override { return responseMode; } }; Define_Module(TestRateSelection); From a6011d5e2491d34914e1b6438168be9a1b74da0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:51:57 +0200 Subject: [PATCH 15/16] ieee80211: add: complete explicit immediate Compressed Block Ack Before this commit, HCF still rejects the Compressed profile. Wire support, explicit receipt state, and rate helpers exist, but production exchanges do not use them. After this commit, explicit Compressed profiles complete immediate exchanges on eligible HT links. Receipt bits survive delivery, absent receipts remain zero, and shifted responses apply only within the active window. Actual physical endpoints establish SIFS or permitted RIFS relations. Active Compressed agreements suppress fragmentation. HCF uses the prepared non-HT BAR rates and 32-byte response airtime. Both endpoints must select the same profile because ADDBA does not negotiate it. Conflicting addressed BAR stops the simulation before agreement updates. Delayed Compressed policy fails initialization. Basic remains the default. Active Compressed DATA checks complete exchange cost before destructive aggregation. Refusal preserves queue frames, deadlines, and sequence state. An oversized aggregate can fall back to its first ordinary MSDU. One selected mode survives RTS/CTS. Failed RTS updates category recovery without a provisional sequence counter. Original lifetimes bound repeated attempts. For example, an aggregate that exceeds the exchange limit leaves its sources intact. HCF can instead admit its first ordinary MSDU with the same selected mode. Commitment records one queue departure and retains its queueing time through retransmission. Direct exchange and admission fixtures check refusal, fallback, retry, and response behavior. Agreement activation, physical interval use, DATA admission, and frame sequences change together to expose a complete supported exchange. Shared queue, timing, permission, wire, preparation, receipt, and rate prerequisites precede activation. Single protection reserves only the current exchange. The following commit changes the default separately. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- ...eee80211-block-ack-agreement-lifecycle.rst | 14 +- ...-compressed-block-ack-extension-points.rst | 22 + .../ieee80211-compressed-control-rates.rst | 4 - .../ieee80211-reversible-data-preparation.rst | 2 +- doc/src/users-guide/ch-80211.rst | 43 +- .../ieee80211-basic-block-ack-selection.txt | 2 +- ...eee80211-block-ack-agreement-lifecycle.txt | 2 +- ...e80211-compressed-control-rate-helpers.txt | 3 - .../ieee80211-compressed-receipt-helpers.txt | 3 - ...ee80211-immediate-compressed-block-ack.txt | 17 + showcases/wireless/blockack/doc/index.rst | 2 +- .../OriginatorBlockAckAgreementHandler.cc | 7 + .../OriginatorBlockAckAgreementPolicy.cc | 2 + .../RecipientBlockAckAgreementPolicy.cc | 2 + .../blockack/RecipientBlockAckProcedure.cc | 8 + .../blockackreordering/BlockAckReordering.cc | 23 +- .../blockackreordering/BlockAckReordering.h | 2 +- .../mac/contract/IRecipientQosAckPolicy.h | 2 + .../contract/IRecipientQosMacDataService.h | 3 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 532 +++++++++++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 38 +- .../mac/coordinationfunction/Hcf.ned | 2 +- .../mac/framesequence/FrameSequenceContext.cc | 20 + .../mac/framesequence/FrameSequenceContext.h | 37 +- .../ieee80211/mac/framesequence/HcfFs.cc | 10 +- .../framesequence/PrimitiveFrameSequences.cc | 43 +- .../ieee80211/mac/framesequence/TxOpFs.cc | 4 +- .../originator/OriginatorQosMacDataService.cc | 2 +- .../originator/OriginatorQosMacDataService.h | 2 + .../ieee80211/mac/originator/QosAckHandler.cc | 27 + .../ieee80211/mac/originator/QosAckHandler.h | 2 + .../SingleProtectionMechanism.cc | 5 +- .../ieee80211/mac/queue/InProgressFrames.cc | 10 + .../ieee80211/mac/queue/InProgressFrames.h | 2 + .../Ieee80211PeerModeSelection.cc | 11 + .../Ieee80211PeerModeSelection.h | 4 + .../mac/recipient/RecipientQosAckPolicy.cc | 9 +- .../mac/recipient/RecipientQosAckPolicy.h | 1 + .../recipient/RecipientQosMacDataService.cc | 20 +- .../recipient/RecipientQosMacDataService.h | 2 +- .../module/Ieee80211BlockAckReordering_1.test | 37 +- .../module/Ieee80211BlockAckScheduling_1.test | 176 ++++- tests/module/Ieee80211BlockAckVariant_1.test | 24 +- ...eee80211CompressedBlockAckAdmission_1.test | 665 ++++++++++++++++++ ...Ieee80211CompressedBlockAckExchange_1.test | 428 +++++++++++ .../Ieee80211CompressedBlockAckInput_1.test | 325 +++++++++ ...ee80211CompressedBlockAckInterframe_1.test | 542 ++++++++++++++ ...ee80211CompressedBlockAckOriginator_1.test | 175 +++++ .../Ieee80211HcfMultipleProtection_2.test | 121 +++- tests/module/Ieee80211RecipientNav_1.test | 97 ++- tests/protocol/wifi/11ac/Ac_BlockAck.test | 55 +- .../wifi/11ac/Ac_BlockAckFallback.test | 186 +++++ tests/protocol/wifi/11n/N_BlockAck.test | 55 +- tests/protocol/wifi/11n/N_CompressedBa.test | 22 +- tests/unit/Ieee80211BlockAckScheduling_1.test | 42 ++ 55 files changed, 3692 insertions(+), 204 deletions(-) create mode 100644 doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst delete mode 100644 doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst delete mode 100644 doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt delete mode 100644 doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt create mode 100644 doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt create mode 100644 tests/module/Ieee80211CompressedBlockAckAdmission_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckExchange_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckInput_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckInterframe_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckOriginator_1.test create mode 100644 tests/protocol/wifi/11ac/Ac_BlockAckFallback.test diff --git a/doc/src/migration-guide/development/ieee80211-block-ack-agreement-lifecycle.rst b/doc/src/migration-guide/development/ieee80211-block-ack-agreement-lifecycle.rst index 167a643998f..00fc546ec16 100644 --- a/doc/src/migration-guide/development/ieee80211-block-ack-agreement-lifecycle.rst +++ b/doc/src/migration-guide/development/ieee80211-block-ack-agreement-lifecycle.rst @@ -1,7 +1,7 @@ IEEE 802.11 Block Ack Agreement Lifecycle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -HCF now defers new DATA extraction for a peer and TID during Block Ack setup or recovery. Already numbered DATA resolves before HCF creates ADDBA. The first DATA under an accepted agreement therefore retains the request sequence number. Basic remains the default. Compressed selection still stops initialization. +HCF now defers new DATA extraction for a peer and TID during Block Ack setup or recovery. Already numbered DATA resolves before HCF creates ADDBA. The first DATA under an accepted agreement therefore retains the request sequence number. Both supported profiles use this lifecycle contract. The recipient response now determines the buffer size and inactivity timeout. The built-in recipient selects the smaller of 64 and its configured maximum. A request size of zero supplies no size guidance. The response copies the request dialog token. Both negotiated A-MSDU flags must permit aggregation under an active agreement. @@ -13,15 +13,15 @@ At ordinary teardown, the originator discards every transmitted, unacknowledged For example, fragment 0 of sequence 4095 enters transmission before teardown. Teardown discards its unsent fragments and preserves the intact successor with sequence 0. The recipient delivers complete buffered units in cyclic order before it releases resources. It discards incomplete units. For example, complete units 101 and 102 can reach the upper process after teardown across absent unit 100. -The QoS data service assigns an increasing allocation index before fragmentation. All fragments retain this index in ``Ieee80211DataSequenceTag``. Custom QoS data services must supply the same metadata. Custom originator code must call the new ``recordTransmittedData(allocationIndex)`` operation when the transmitter accepts DATA. The agreement exposes its highest transmitted index through the new ``getHighestTransmittedDataIndex()`` query. HCF supplies this integration for the built-in implementation. +Custom originator code must call ``recordTransmittedData(allocationIndex)`` instead of ``markFirstDataTransmitted()`` or the former cyclic-sequence overload. The QoS data service assigns an increasing local index before fragmentation. All fragments retain this index in ``Ieee80211DataSequenceTag``. Custom QoS data services must supply the same metadata. The agreement exposes its highest transmitted index through ``getHighestTransmittedDataIndex()``, which replaces ``getHighestTransmittedSequence()``. HCF records this index when the transmitter accepts DATA. The index remains available after ACK processing removes a frame. Teardown compares allocation indices to remove unsent fragments of a partially transmitted unit. This comparison remains valid across sequence gaps and wraparound. For example, transmission of sequences 0 and 2048 updates the boundary correctly when intervening DATA receives allocation indices but never transmits. Retransmission of older DATA cannot move the boundary backward. -Association reset instead discards all buffered units from the old association. Management calls ``IIeee80211Mac::resetPeerAssociationContext()`` with the old peer address before replacement. This C++ interface pairs with the existing ``IIeee80211Mac`` NED module interface. The MAC implements this contract and forwards the reset to HCF. Custom management implementations must supply this handoff for association replacement and loss. A rate-state notification alone cannot identify replacement with identical capabilities. +Association reset instead discards all buffered units from the old association. Management calls ``IBlockAckAgreementLifecycle::resetPeerAgreements()`` with the old peer address before replacement. The MAC implements this contract and forwards the reset to HCF. Custom management implementations must supply this handoff for association replacement and loss. A rate-state notification alone cannot identify replacement with identical capabilities. With Block Ack support enabled, peer reset also discards queued and in-progress QoS DATA addressed to the old peer. This includes unsent Normal Ack DATA without an agreement. Accepted physical exchanges retain their context until safe completion. Reset preserves other peers' queued DATA and lifetime records. -Custom MAC implementations that use these management modules must implement the new C++ ``IIeee80211Mac`` interface from ``inet/linklayer/ieee80211/IIeee80211Mac.h``. This interface extends ``IMacProtocol`` and adds ``resetPeerAssociationContext(peer)`` for management commands. Management selects the old association context; MAC components own its cleanup. Custom management code must use the new header and interface for direct reset calls. The reset must complete both agreement roles' cleanup before it reports deletion. It must preserve an unrelated management exchange that already holds an accepted transmission. Implementations without Block Ack support perform no cleanup. +Custom MAC implementations that use these management modules must implement ``IBlockAckAgreementLifecycle``. The reset must complete both agreement roles' cleanup before it reports deletion. It must preserve an unrelated management exchange that already holds an accepted transmission. ``blockAckAgreementDeleted`` now supplies a nonnull agreement after removal from lookup and completion of the required cleanup. The payload identifies the selected role through its agreement class. The payload remains valid only during the synchronous callback. Listeners must not retain or modify it. Association reset removes both roles before the first callback. Ordinary directional teardown preserves the opposite role. @@ -35,10 +35,10 @@ Custom implementations of ``IBlockAckAgreementHandlerCallback`` must implement ` Custom subclasses of ``OriginatorQosMacDataService`` must adapt the protected ``aMsduAggregateIfNeeded(queue, selected)`` helper. The selected packet is the first eligible packet in service order. An aggregate candidate list must start with that packet. The service validates every candidate before source removal and calls the aggregation policy once. -HCF uses the existing filtered ``canPullPacket(gate, filter)`` operation to find unexpired DATA for a peer and TID. Custom pending queues must support this query. A query returns a borrowed packet without a change to queue state. The filter must accept ``const Packet *``. The eligibility callback in ``OriginatorQosMacDataService::setEligibility()`` also accepts ``const Packet *``. +Custom queues need ``inet::queueing::ISelectivePacketCollection`` only when a deferred next frame hides eligible work. HCF retains ordinary dequeue when the advertised next frame is eligible. It waits when every frame is deferred. It reports the missing capability before mutation when eligible work requires a bypass. -The data service retains ordinary dequeue when the advertised next frame is eligible. Otherwise, it supplies an eligibility filter to ``canPullPacket()``. If the query finds no eligible frame, the service waits. If the query finds an eligible frame, ``pullPacket(gate, filter)`` transfers that frame through an identity filter. For example, deferred DATA for one peer can precede eligible DATA for another peer. The pull removes only the eligible DATA. The deferred DATA retains its sequence state and lifetime. +The optional ``peekEligiblePacket(predicate)`` operation returns a borrowed packet in configured service order without mutation. ``extractPacket(packet)`` transfers that exact packet to its caller. Implementations must preserve their ordinary pull effects, including ownership, residence metadata, events, statistics, and applicable animation. Built-in simple queues, priority schedulers, and compound queues support this contract. Custom subclasses that change pull effects must adapt selected extraction. The optional contract supports complete packets only. -Custom queues must preserve ordinary pull effects for filtered pulls, including ownership, residence metadata, events, statistics, and applicable animation. Built-in queues, priority schedulers, and compound queues already support these operations. The service propagates queue and filter errors. It does not replace a failed pull with collection removal. These operations transfer complete packets. +Providers that cannot perform a selected operation must throw ``inet::queueing::UnsupportedPacketSelection``. This exception permits ordinary service and collection inspection without the optional capability. Genuine queue and predicate errors must retain their original type. External frame-sequence handlers must implement ``interruptFrameSequence()``. The handler owns the interruption outcome after recipient processing. It suppresses failure feedback when the transmitted DATA or management frame no longer belongs to the in-progress queue. Other exchanges retain ordinary failure feedback. Cleanup must destroy the exchange context before it deletes deferred frame copies. These repairs do not add MAC stop/start recovery or final-transfer teardown. diff --git a/doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst b/doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst new file mode 100644 index 00000000000..0f9cbdce7b5 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst @@ -0,0 +1,22 @@ +IEEE 802.11 Compressed Block Ack extension points +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Both endpoints must select the same profile. ADDBA does not negotiate this selector. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, local profile, and received profile. Configure the same selectors before a new run. + +Compressed agreements require usable local HT support and directional peer HT state. An HT PHY mode alone does not supply that state. Without it, HCF retains Normal Ack DATA. Explicit ``blockAckVariant = "basic"`` retains historical Basic compatibility without that HT requirement. Compressed agreements suppress fragmentation for their peer and TID. Fragmentation outside those agreements retains its existing threshold. + +Compressed supports immediate Block Ack only. Keep ``delayedAckPolicySupported = false`` in both agreement policies. HCF rejects either true value during initialization when Compressed support is enabled. Basic retains its existing delayed-policy configuration contract. + +Custom QoS rate selectors must implement ``computeCompressedBlockAckReqMode(packet, header, startsTxop)``. HCF supplies the request's actual position in the TXOP. Select a legal non-HT mode from the applicable BSS or peer rates. A configured ``controlFrameBitrate`` must satisfy that rule. The existing Basic rate-selection methods remain available. + +Custom recipient ACK policies must implement ``computeCompressedBlockAckDurationField()``. This operation subtracts SIFS and the actual Compressed response airtime from the request reservation. Compressed Block Ack contains 32 bytes with FCS; Basic contains 152 bytes. Retain the existing Basic operation. + +Custom recipient data services must accept ``bool interframe = false`` in ``dataFrameReceived()``. The value states whether HCF established the required SIFS or RIFS relation. A false value supplies no interval evidence. Preserve that value through the recipient reorder path. Custom callers that omit it retain false. + +Custom frame sequences must supply HCF's resolved profile to ``QoSContext``. Its constructor retains Basic for callers that omit the profile. Custom agreement creation must set the recipient agreement's variant before use. An agreement must retain that variant throughout its lifetime. + +Custom frame sequences must preserve BAR arbitration before DATA preparation. The HCF preparation callback distinguishes the legacy path, accepted preparation, and admission refusal. Refusal must end the sequence without a DATA failure. Keep the selected DATA mode through RTS/CTS. Validate that decision without another adaptive rate query. Admit the complete exchange before source removal. + +The active Compressed path reserves only the current exchange under single protection. A later BAR can require fresh contention. Before DATA commitment, failed RTS updates QSRC and the contention window without a provisional sequence counter. QSRC is the short retry counter for an access category. QSRC rollover does not discard queued DATA. Original lifetimes bound repeated RTS attempts. + +Basic and outside-agreement fallback retain their existing preparation and recovery paths. diff --git a/doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst b/doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst deleted file mode 100644 index 8852fbf1ffd..00000000000 --- a/doc/src/migration-guide/development/ieee80211-compressed-control-rates.rst +++ /dev/null @@ -1,4 +0,0 @@ -IEEE 802.11 Compressed control rates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Custom QoS rate selectors must implement ``computeCompressedBlockAckReqMode(packet, header, startsTxop)``. This operation prepares legal non-HT BAR rate selection from the supplied TXOP position and applicable BSS or peer rates. HCF retains its Compressed-profile rejection in this prerequisite. Runtime activation will supply the request's actual TXOP position. Existing Basic methods retain their contracts. diff --git a/doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst b/doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst index cc75b3d75f4..13ac98f2734 100644 --- a/doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst +++ b/doc/src/migration-guide/development/ieee80211-reversible-data-preparation.rst @@ -1,4 +1,4 @@ IEEE 802.11 reversible DATA preparation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Custom QoS data services must implement the reversible preparation operations in ``IOriginatorQosMacDataService``. ``prepareFrame()`` creates an owned preview and retains original MSDUs in their queue. ``isPreparationValid()`` checks current source identity, eligibility, lifetime, sequence state, and aggregation permission. ``commitPreparedFrame()`` transfers the same preview pointer only after successful validation. Failure must preserve the preview and all original queue frames. +Custom QoS data services must implement the reversible preparation operations in ``IOriginatorQosMacDataService``. ``prepareFrame()`` creates an owned preview and retains original MSDUs in their queue. ``isPreparationValid()`` checks current source identity, eligibility, lifetime, sequence state, and aggregation permission. ``commitPreparedFrame()`` transfers the same preview pointer only after successful validation. Failure must preserve the preview and all original queue frames. ``InProgressFrames::adoptPreparedFrame()`` accepts a committed frame without another extraction. diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index b526cdbf248..e6f09b57bd6 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -533,7 +533,7 @@ The first exchange proceeds even when its cost exceeds a positive TXOP limit. Th A zero limit permits the initial exchange and its existing RTS/CTS and immediate response. Each later fragment or unrelated unit needs a fresh TXOP. Prepared fragments and A-MSDUs use the same complete-cost check under positive limits. -``isBlockAckSupported = true`` disables complete-cost continuation admission, even without a Block Ack agreement. Multiple protection also retains its separate admission policy. Both paths use shared TXNAV. Basic Block Ack admission, BAR forecasts, zero-limit companion rules, and optional overrun exceptions remain outside this repair. +With Block Ack support enabled, active Compressed agreements use the admission described in :ref:`ug:sec:80211:blockack`. Basic and traffic outside those agreements retain their existing admission policy. Those retained paths can exceed DATA exchange bounds. All paths use shared TXNAV. Basic Block Ack admission and BAR forecasts remain outside the Compressed correction. All single protection paths classify group frames before the current-frame ACK equation. With Block Ack support disabled, the current one-frame group sequence advertises zero. With support enabled, its legacy forecast can reserve a queued successor that the sequence cannot transmit. This limit applies to group management and non-QoS DATA too. @@ -607,13 +607,9 @@ The standard separately requires AIFSN of at least 1 for APs and at least 2 for example, a non-AP station with AIFSN 1 meets the ownership condition but violates the standard's minimum. -Multiple protection does not check a complete exchange against the remaining budget. -For example, its admission policy can start a 120 us exchange with only -100 us left. The reference scope assumes whole-microsecond positive limits, integral initial PPDU -airtime, and integral legacy RTS airtime. It adds no A-MPDU, Compressed Block Ack, new -fragmentation, CTS-to-self, dual CTS, PIFS recovery, or TXOP sharing. It also adds no reverse -direction, beamforming, S1G, multi-user, or general MAC lifecycle support. It does not repair -general rate selection, recipient response timing, or TXOP overruns. +Outside active Compressed agreements, multiple protection does not check complete exchange cost against the remaining budget. For example, that retained policy can start a 120 us exchange with only 100 us left. Active Compressed agreements use their separate complete-cost admission. The reference scope assumes whole-microsecond positive limits, integral initial PPDU airtime, and integral legacy RTS airtime. + +Multiple protection adds no A-MPDU, new fragmentation, CTS-to-self, dual CTS, PIFS recovery, or TXOP sharing. Reverse direction, beamforming, S1G, multi-user operation, and general MAC lifecycle support remain outside this implementation. General rate selection and recipient response timing retain their separate limits. .. _ug:sec:80211:fragmentation: @@ -708,12 +704,27 @@ BAR can obtain channel access without a queued DATA frame. A later BAR within a After the final exchange, the originator sends DELBA when its agreement has no queued DATA, in-progress DATA, or active BAR exchange. A zero inactivity timeout does not disable this transfer-completion rule. DATA that arrives after this boundary waits for DELBA completion. A later transfer can require another ADDBA exchange. If the last unresolved DATA expires, the originator uses the same teardown path. Ordinary recipient teardown releases complete units behind a receive gap before it releases agreement resources. -The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and -``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size -(:par:`maximumAllowedBufferSize`, default 64), the A-MSDU support -(:par:`aMsduSupported`, default ``true``) and the inactivity timeout -(:par:`blockAckTimeoutValue`, default ``0s``, which means no timeout). Only -the basic Block Ack variant is modeled. +The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and ``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size. ``maximumAllowedBufferSize`` defaults to 64. Both negotiated ``aMsduSupported`` flags control A-MSDU use; each defaults to ``true``. ``blockAckTimeoutValue`` defaults to ``0s``, which disables the inactivity timeout. Transfer completion and DATA lifetime expiry retain their separate teardown rules. + +Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Block Ack support itself remains disabled by default. The response contains a 64-bit bitmap and occupies 32 bytes with FCS. Each positive bit acknowledges one unfragmented MPDU. The recipient retains receipt bits after delivery, so a later BAR can still obtain those positive bits. Missing receipt evidence produces zero. + +Compressed agreements require usable local HT support and directional peer HT state. Infrastructure association supplies that peer state for eligible HT links and the supported ``ac`` path. An HT radio mode alone does not supply peer capabilities. Without usable state, DATA uses Normal Ack. Local non-HT operation produces a warning when Block Ack support is enabled. HT ad hoc links without peer state retain Normal Ack. + +Explicit ``blockAckVariant = "basic"`` selects historical Basic compatibility on both endpoints. Basic retains its 64 fragment bitmap entries and its 152-byte response with FCS. IEEE Std 802.11e-2005 introduced this format. IEEE Std 802.11-2024 reserves its Type 0 value. INET retains Basic for existing studies; this compatibility does not establish complete historical conformance. + +Both endpoints must select the same profile because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Configure matching profiles before a new run. Malformed frames follow input rejection instead of this configuration diagnostic. + +Compressed agreements suppress fragmentation for their peer and TID. The recipient rejects fragmented DATA under those agreements before receipt or reorder updates. Basic retains fragmentation. Outside a Compressed agreement, the configured fragmentation threshold retains its existing effect. + +Keep ``delayedAckPolicySupported = false`` in both agreement policies for Compressed support. HCF rejects true at initialization because the Compressed path supplies immediate responses only. Retained Basic delayed-policy configuration does not establish a complete delayed exchange implementation. + +The Compressed originator sends BAR with a non-HT PHY mode. Initial BAR uses the applicable BSS basic or mandatory rate rule. Later BAR uses the peer operational rate rule. A configured ``controlFrameBitrate`` must satisfy that rule. The response selector uses the actual 32-byte response length when a legal alternate rate is available. These rules retain separate Basic, ACK, and CTS contracts. + +Active Compressed agreements check the complete DATA exchange before source commitment. If an aggregate cannot qualify, HCF evaluates its first ordinary MSDU with the same selected DATA mode. If neither form qualifies, HCF ends the sequence without queue removal, sequence assignment, or deadline replacement. RTS/CTS retains the selected mode. Expiry or capability changes before DATA cancel invalid preparation. + +An initial unaggregated MSDU can use the applicable TXOP overrun exception. A same-size single-MPDU retransmission can also use its exception. That retransmission can carry an A-MSDU. Each exception permits at most one Data or Management frame in that TXOP. A new A-MSDU must fit a positive limit. A zero limit permits one DATA PPDU with one MSDU or A-MSDU, plus its required protection and response. + +Single protection reserves only the current Compressed exchange. A later BAR can require fresh contention. This policy can change airtime and delivery times. Before DATA commitment, failed RTS updates QSRC and the contention window without a provisional sequence counter. QSRC is the short retry counter for an access category. Original MSDU lifetimes bound repeated attempts; QSRC rollover does not discard them. .. code-block:: ini @@ -1030,9 +1041,9 @@ The model does not contain: - PCF and HCCA, the polled channel access methods. The module :ned:`Hcca` exists as a placeholder. -- A-MPDU aggregation, the MSDU lifetime limit, and self-CTS protection. +- A-MPDU aggregation and self-CTS protection. -- Compressed and multi-TID block acknowledgement. +- Multi-TID block acknowledgement and delayed Compressed Block Ack. - Operational HT widths above 20 MHz, and a primary/secondary channel relationship for wider VHT modes. diff --git a/doc/whatsnew/compatible/ieee80211-basic-block-ack-selection.txt b/doc/whatsnew/compatible/ieee80211-basic-block-ack-selection.txt index a6f1f632bbc..b24eea4a527 100644 --- a/doc/whatsnew/compatible/ieee80211-basic-block-ack-selection.txt +++ b/doc/whatsnew/compatible/ieee80211-basic-block-ack-selection.txt @@ -1,3 +1,3 @@ Explicit historical Basic Block Ack selection -HCF adds blockAckVariant = "basic" for historical compatibility. Existing Block Ack, TXOP, QoS, and fragmentation demonstrations select it explicitly. Basic remains the default. HCF rejects unknown values and the reserved "compressed" value during initialization until Compressed exchanges exist. These checks also apply when Block Ack support is disabled. Basic originated in IEEE Std 802.11e-2005; IEEE Std 802.11-2024 reserves its type value. +HCF retains blockAckVariant = "basic" for historical compatibility. Existing Block Ack, TXOP, QoS, and fragmentation demonstrations select it explicitly. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. Basic originated in IEEE Std 802.11e-2005; IEEE Std 802.11-2024 reserves its type value. diff --git a/doc/whatsnew/incompatible/ieee80211-block-ack-agreement-lifecycle.txt b/doc/whatsnew/incompatible/ieee80211-block-ack-agreement-lifecycle.txt index 8c49e3aebbf..6375b44236b 100644 --- a/doc/whatsnew/incompatible/ieee80211-block-ack-agreement-lifecycle.txt +++ b/doc/whatsnew/incompatible/ieee80211-block-ack-agreement-lifecycle.txt @@ -12,4 +12,4 @@ Originator retirement now uses local allocation indices to preserve this boundar The recipient delivers complete buffered units in cyclic order across receive gaps before resource release. Association reset instead discards old buffered units in both directions. With Block Ack enabled, reset also discards queued and in-progress QoS DATA for the old peer, including unsent Normal Ack DATA. These changes can alter delivery, disposal, frame exchanges, and fingerprints. -Basic remains the default. Compressed selection still stops initialization. Custom agreement handlers, recipient services, and management implementations require the changes in the migration guide. HCF uses filtered canPullPacket() queries to find queued DATA. The data service uses filtered pullPacket() calls when deferred DATA hides eligible work. Custom queues must support these operations. Built-in queues preserve ordinary pull effects. +Custom agreement handlers, recipient services, and management implementations require the changes in the migration guide. External queues need the optional selected-extraction contract only when deferred DATA hides eligible work. Built-in queues preserve ordinary pull accounting for that extraction. diff --git a/doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt b/doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt deleted file mode 100644 index 34a489d57cf..00000000000 --- a/doc/whatsnew/incompatible/ieee80211-compressed-control-rate-helpers.txt +++ /dev/null @@ -1,3 +0,0 @@ -Compressed control-rate helpers - -QoS rate selection now supplies non-HT Compressed BAR and response modes. HCF still refuses the Compressed profile until complete exchange support exists. diff --git a/doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt b/doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt deleted file mode 100644 index 65894263aca..00000000000 --- a/doc/whatsnew/incompatible/ieee80211-compressed-receipt-helpers.txt +++ /dev/null @@ -1,3 +0,0 @@ -Compressed receipt helpers - -Compressed receipt helpers now retain positive receipt state independently of delivery. HCF still refuses the Compressed profile until complete exchange support exists. diff --git a/doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt b/doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt new file mode 100644 index 00000000000..47d15c2b366 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt @@ -0,0 +1,17 @@ +IEEE 802.11 immediate Compressed Block Ack + +HCF supports explicit immediate Compressed Block Ack. Basic remains the default. Eligible HT infrastructure links and the supported ac path use accepted agreements and 32-byte Compressed responses with FCS. Links without usable local or peer HT state retain Normal Ack. Select blockAckVariant = "basic" at both endpoints for historical Basic compatibility. An HT PHY mode alone does not supply peer capability state. + +Both endpoints must select the same blockAckVariant because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Compressed rejects delayedAckPolicySupported = true in either agreement policy at initialization. + +Compressed receipt state retains positive bits after delivery and reports zero for absent receipts. HCF recognizes the required SIFS or RIFS relation from actual physical endpoints. It can release individual MPDUs under that relation despite a receive gap. Compressed agreements suppress fragmentation for their peer and TID; Basic retains its fragment bitmap. + +Compressed BAR uses a legal non-HT rate. Initial and later BAR consume their applicable BSS and peer rate sets. A configured controlFrameBitrate must satisfy the applicable rule. Response duration, protection, and timeout calculations use the 32-byte response length. Basic retains its separate 152-byte response. + +Active Compressed agreements check complete DATA exchange cost before destructive aggregation. If an aggregate cannot qualify, HCF evaluates its first ordinary MSDU with the same selected mode. Refusal preserves original queue frames, deadlines, and sequence state. Single protection reserves only the current exchange; a later BAR can require fresh contention. These changes can alter airtime, contention, delivery times, and simulation results. Basic and traffic outside active Compressed agreements retain their existing admission limits. + +Before DATA commitment, failed RTS updates access-category retry state without a provisional sequence counter. Original DATA lifetimes bound repeated attempts. This changes the scoped path's former discard after a fixed number of RTS failures. + +Existing demonstrations retain explicit Basic settings, configurations, and result rows. IEEE Std 802.11e-2005 introduced Basic; IEEE Std 802.11-2024 reserves its Type 0 value. Retention does not establish complete historical conformance. A-MPDU requirements for n and ac remain deferred. Multi-TID Block Ack and the existing delayed Basic requirement also remain deferred. Compressed support does not establish complete VHT transfer support or MAC stop/start recovery. + +Custom QoS rate selectors, recipient ACK policies, recipient data services, and frame sequences require the interface changes in the migration guide. Custom radio media, communication caches, and IEEE 802.11 modes also require the new timing queries. Basic classes and methods remain available. diff --git a/showcases/wireless/blockack/doc/index.rst b/showcases/wireless/blockack/doc/index.rst index 1418ec4a985..2d6aa48f04e 100644 --- a/showcases/wireless/blockack/doc/index.rst +++ b/showcases/wireless/blockack/doc/index.rst @@ -50,7 +50,7 @@ Block Acknowledgment in INET In INET, the block ack mechanism can be enabled by setting the :par:`isBlockAckSupported` parameter of the MAC's :ned:`Hcf` submodule to ``true`` (it is disabled by default). -Select ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. Basic remains the default during preparation for Compressed support. This stage rejects ``"compressed"`` because its runtime exchange path does not yet exist. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. +This showcase selects ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. Basic remains the default. Other configurations can select Compressed explicitly. Compressed agreements require usable local and peer HT state. Without that state, DATA uses Normal Ack. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. Several submodules of :ned:`Hcf` are involved in the block ack mechanism. These modules have parameters controlling the block ack behavior. The following paragraphs diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index e136518a229..c526b7fe88c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -174,6 +174,13 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { + auto agreement = getAgreement(compressed->getTransmitterAddress(), compressed->getTidInfo()); + if (agreement) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } + } else throw cRuntimeError("Unsupported BlockAck"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 835216df923..1a03dd26ef2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -41,6 +41,8 @@ simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const bool OriginatorBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const { + if (variant == BlockAckVariant::COMPRESSED && (!localHt || !peerHt)) + EV_DEBUG << "Compressed Block Ack setup refused: " << (!localHt ? "local HT state is absent" : "peer HT state is absent") << "; DATA uses Normal Ack.\n"; return variant == BlockAckVariant::BASIC || (localHt && peerHt); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index f2bd97fa697..33360fcfc32 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc @@ -39,6 +39,8 @@ bool RecipientBlockAckAgreementPolicy::isAddbaReqAccepted(const PtrtransmitControlResponseFrame(blockAckPacket, blockAck, blockAckPacketReq, basicBlockAckReq); } } + else if (auto compressed = dynamicPtrCast(blockAckReq)) { + auto agreement = blockAckAgreementHandler->getAgreement(compressed->getTidInfo(), compressed->getTransmitterAddress()); + if (ackPolicy->isBlockAckNeeded(compressed, agreement)) { + auto blockAck = buildBlockAck(compressed, agreement); + blockAck->setDurationField(ackPolicy->computeCompressedBlockAckDurationField(blockAckPacketReq, compressed)); + callback->transmitControlResponseFrame(new Packet("CompressedBlockAck", blockAck), blockAck, blockAckPacketReq, compressed); + } + } else throw cRuntimeError("Unsupported BlockAckReq"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 02a811e4732..78f06a2d725 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -50,11 +50,12 @@ void BlockAckReordering::clearPeer(MacAddress peer) } } -BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe) { ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); auto sequenceNumber = dataHeader->getSequenceNumber(); auto windowStart = receiveBuffer->getNextExpectedSequenceNumber(); + bool insideWindow = windowStart <= sequenceNumber && sequenceNumber < windowStart + receiveBuffer->getBufferSize(); ReleasedFrames releasedFrames; // IEEE Std 802.11-2024, 10.25.6.6.2.1(b,c): advance for newer DATA beyond the window; discard older DATA. // The shared reorder service also retains Basic fragment reassembly as an INET compatibility choice. @@ -69,8 +70,15 @@ BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(R // 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) + if (dataHeader->getAckPolicy() == BLOCK_ACK && agreement->getVariant() == BlockAckVariant::BASIC) agreement->blockAckPolicyFrameReceived(dataHeader); + auto record = agreement->getBlockAckRecord(); + // IEEE Std 802.11-2024, 10.25.6.6.2.1(a)(2)(v) and 10.25.6.6.1: release the complete prefix in order. + if (agreement->getVariant() == BlockAckVariant::COMPRESSED && insideWindow && windowStart < sequenceNumber && + interframe && record->hasCompleteReceipt(record->getWindowStart() - 1)) { + auto preceding = advanceReceiveBuffer(agreement, receiveBuffer, sequenceNumber); + releasedFrames.insert(releasedFrames.end(), preceding.begin(), preceding.end()); + } // IEEE Std 802.11-2024, 10.25.6.6.2.1(a)(2): release every consecutive complete unit at the window start. auto consecutiveFrames = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); releaseReceiveBuffer(agreement, receiveBuffer, consecutiveFrames); @@ -92,6 +100,14 @@ BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedBlockAckRe return ReleasedFrames(); startingSequenceNumber = basicReq->getStartingSequenceNumber(); } + else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { + if (compressedReq->getFragmentNumber() != 0 || compressedReq->getMultiTid() || !compressedReq->getCompressedBitmap() || + compressedReq->getTidInfo() < 0 || compressedReq->getTidInfo() > 15 || !compressedReq->getStartingSequenceNumber().isSpecified() || + compressedReq->getChunkLength() != LENGTH_BLOCKACKREQ - B(4)) + return ReleasedFrames(); + startingSequenceNumber = compressedReq->getStartingSequenceNumber(); + agreement->getBlockAckRecord()->advanceCompressedWindow(startingSequenceNumber); + } else { throw cRuntimeError("Multi-Tid BlockAckReq is currently an unimplemented feature"); } @@ -209,7 +225,8 @@ void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, Receive receiveBuffer->setNextExpectedSequenceNumber(sequenceNumber + 1); receiveBuffer->dropFramesUntil(sequenceNumber); receiveBuffer->removeFrame(sequenceNumber); - agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); + if (agreement->getVariant() == BlockAckVariant::BASIC) + agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); } std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 05a203ed44b..fdfbd29e226 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -54,7 +54,7 @@ class INET_API BlockAckReordering void clearPeer(MacAddress peer); void processReceivedDelba(const Ptr& delba); - ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); + ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe = false); ReleasedFrames processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h index ccf47cc5610..42aaecc9db8 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h @@ -26,6 +26,8 @@ class INET_API IRecipientQosAckPolicy virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const = 0; virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const = 0; + // The request selects the response variant and its actual airtime. + virtual simtime_t computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 7e97329e94d..7aded713794 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -26,7 +26,8 @@ class INET_API IRecipientQosMacDataService virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) = 0; virtual void clearPeer(MacAddress peer) = 0; - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; + // HCF supplies the observed SIFS or permitted RIFS relation for this frame. + virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false) = 0; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 1c8dbbdf508..36c282f8202 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -18,12 +18,17 @@ #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/rateselection/Ieee80211PeerModeSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/queueing/function/PacketFilterFunction.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" namespace inet { namespace ieee80211 { @@ -47,8 +52,6 @@ void Hcf::initialize(int stage) blockAckVariant = BlockAckVariant::COMPRESSED; else throw cRuntimeError("Unknown blockAckVariant \"%s\": expected \"basic\" or \"compressed\"", configuredVariant); - if (blockAckVariant == BlockAckVariant::COMPRESSED) - throw cRuntimeError("Compressed Block Ack exchanges are not implemented"); mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); mib = check_and_cast(mac->getModuleByPath(mac->par("mibModule"))); startRxTimer = new cMessage("startRxTimeout"); @@ -65,6 +68,7 @@ void Hcf::initialize(int stage) rateSelection = check_and_cast(getSubmodule("rateSelection")); frameSequenceHandler = new FrameSequenceHandler(); WATCH_EXPR("frameSequenceInfo", getFrameSequenceInfo()); + WATCH_EXPR("numPendingInterframeReceptions", pendingReceptions.size()); originatorDataService = check_and_cast(getSubmodule(("originatorMacDataService"))); recipientDataService = check_and_cast(getSubmodule("recipientMacDataService")); originatorAckPolicy = check_and_cast(getSubmodule("originatorAckPolicy")); @@ -83,6 +87,23 @@ void Hcf::initialize(int stage) } } else if (stage == INITSTAGE_LAST) { + if (blockAckVariant == BlockAckVariant::COMPRESSED && par("isBlockAckSupported").boolValue()) { + interframeRadio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto radioModule = check_and_cast(interframeRadio.get()); + for (auto signal : {receptionStartedSignal, receptionEndedSignal, transmissionStartedSignal, transmissionEndedSignal, IRadio::receptionStateChangedSignal}) + radioModule->subscribe(signal, this); + } + if (par("isBlockAckSupported").boolValue()) { + EV_INFO << "Block Ack profile: " << (blockAckVariant == BlockAckVariant::BASIC ? "basic" : "compressed") << endl; + if (blockAckVariant == BlockAckVariant::COMPRESSED) { + if (originatorBlockAckAgreementPolicy && originatorBlockAckAgreementPolicy->isDelayedAckPolicySupported()) + throw cRuntimeError("Compressed Block Ack does not support delayedAckPolicySupported in %s.originatorBlockAckAgreementPolicy", getFullPath().c_str()); + if (recipientBlockAckAgreementPolicy && recipientBlockAckAgreementPolicy->delayedBlockAckPolicySupported()) + throw cRuntimeError("Compressed Block Ack does not support delayedAckPolicySupported in %s.recipientBlockAckAgreementPolicy", getFullPath().c_str()); + if (!mib->isHtOperationSupported()) + EV_WARN << "Compressed Block Ack has no usable local HT support; DATA uses Normal Ack. Set blockAckVariant = basic for Basic compatibility.\n"; + } + } // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. auto lifetimeHandler = edca->getLifetimeHandler(); @@ -92,6 +113,13 @@ void Hcf::initialize(int stage) check_and_cast(originatorDataService)->setEligibility( [this](const Packet *packet) { return isFrameEligible(packet, false); }, [this](Packet *packet) { return isAMsduPermitted(packet); }); + check_and_cast(originatorDataService)->setFragmentationPermission([this](Packet *packet) { + auto data = dynamicPtrCast(packet->peekAtFront()); + if (blockAckVariant != BlockAckVariant::COMPRESSED || !data || data->getType() != ST_DATA_WITH_QOS || !originatorBlockAckAgreementHandler) + return true; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + return !agreement || !agreement->getIsAddbaResponseReceived(); + }); mib->subscribe(Ieee80211Mib::rateStateChangedSignal, this); for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { edca->getEdcaf(static_cast(ac))->setFrameEligibility( @@ -310,7 +338,59 @@ void Hcf::processUpperFrame(Packet *packet, const Ptr(interframeRadio.get()) && signalID == receptionStartedSignal) { + auto reception = check_and_cast(obj); + auto transmission = reception->getTransmission(); + std::set active; + for (auto ongoing : interframeRadio->getReceptionsInProgress()) + active.insert(ongoing->getId()); + for (auto it = pendingReceptions.begin(); it != pendingReceptions.end();) { + if (!it->second.completed && !active.count(it->first)) + it = pendingReceptions.erase(it); + else + ++it; + } + PendingReception pending; + auto medium = interframeRadio->getMedium(); + pending.timing = medium->getTransmissionTimingSnapshot(transmission->getId()); + auto wifi = dynamic_cast(transmission); + pending.ht = wifi && wifi->getMode()->getModulationClass() == IIeee80211Mode::ModulationClass::HT; + if (wifi) + pending.extension = wifi->getSignalExtensionDuration(); + auto gap = medium->computePrecedingSignalGap(transmission->getId(), interframeHistory.timing); + if (gap && (interframeHistory.mpdu || interframeHistory.ack)) { + // IEEE Std 802.11-2024, 10.3.2.3.3: this model uses coverage class zero. + pending.sifs = std::abs((*gap - modeSet->getSifsTime()).raw()) <= modeSet->getSlotTime().raw() / 10; + if (interframeHistory.mpdu && interframeHistory.ht && pending.ht && interframeHistory.noSifsResponse && + interframeHistory.timing.transmitterRadioId == pending.timing.transmitterRadioId && interframeHistory.extension) { + auto rifs = wifi->getMode()->getRifsTime(); + // IEEE Std 802.11-2024, 10.3.2.3.2 and 10.3.8: RIFS excludes the actual preceding extension. + pending.rifs = std::abs((*gap + *interframeHistory.extension - rifs).raw()) <= rifs.raw() / 10; + pending.precedingReceiver = interframeHistory.receiver; + } + } + interframeHistory = {}; + pendingReceptions.insert_or_assign(transmission->getId(), std::move(pending)); + } + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == receptionEndedSignal) { + auto reception = check_and_cast(obj); + auto pending = pendingReceptions.find(reception->getTransmission()->getId()); + if (pending != pendingReceptions.end()) + pending->second.completed = true; + } + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == transmissionStartedSignal) + clearInterframeHistory(); + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == transmissionEndedSignal) { + clearInterframeHistory(); + auto transmission = check_and_cast(obj); + if (completedTransmissionWasAck) { + interframeHistory.timing = interframeRadio->getMedium()->getTransmissionTimingSnapshot(transmission->getId()); + interframeHistory.ack = true; + } + completedTransmissionWasAck = false; + } + else if (signalID == InProgressFrames::packetDequeuedSignal) { Enter_Method("packetDequeued"); edca->getLifetimeHandler()->forgetFrame(check_and_cast(obj)->getId()); } @@ -329,6 +409,56 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, ModeSetListener::receiveSignal(source, signalID, obj, details); } +void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) +{ + Enter_Method_Silent(); + if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == IRadio::receptionStateChangedSignal && + value != IRadio::RECEPTION_STATE_RECEIVING) { + bool incomplete = false; + for (auto it = pendingReceptions.begin(); it != pendingReceptions.end();) { + if (!it->second.completed) { + incomplete = true; + it = pendingReceptions.erase(it); + } + else + ++it; + } + if (incomplete || value == IRadio::RECEPTION_STATE_BUSY || value == IRadio::RECEPTION_STATE_UNDEFINED) + interframeHistory = {}; + } +} + +void Hcf::clearInterframeHistory() +{ + interframeHistory = {}; + pendingReceptions.clear(); +} + +bool Hcf::receiveInterframe(Packet *packet, const Ptr& header) +{ + auto time = packet->findTag(); + if (!time) + return false; + auto entry = pendingReceptions.find(time->getTransmissionId()); + if (entry == pendingReceptions.end() || !entry->second.completed) + return false; + auto pending = std::move(entry->second); + pendingReceptions.erase(entry); + // IEEE Std 802.11-2024, 10.27.3.1, 10.27.3.3, and 11.38.1: the BSS controls RIFS permission. + bool rifsPermitted = pending.rifs && modeSet->getPhyType() != Ieee80211ModeSet::PhyType::VHT && + (mib->mode == Ieee80211Mib::INDEPENDENT || mib->mode == Ieee80211Mib::MESH || mib->getHtOperation().rifsMode); + bool related = pending.sifs || (pending.rifs && rifsPermitted && pending.precedingReceiver == header->getReceiverAddress()); + interframeHistory.timing = std::move(pending.timing); + interframeHistory.mpdu = true; + interframeHistory.ht = pending.ht; + interframeHistory.receiver = header->getReceiverAddress(); + interframeHistory.extension = pending.extension; + auto dataOrMgmt = dynamicPtrCast(header); + interframeHistory.noSifsResponse = dataOrMgmt ? !recipientAckPolicy->isAckNeeded(dataOrMgmt) : + !dynamicPtrCast(header) && !dynamicPtrCast(header); + return related; +} + void Hcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -562,6 +692,14 @@ void Hcf::completeTransfers() void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex) { + if (frameSequenceHandler->isSequenceRunning()) { + auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); + if (prepared) { + auto header = prepared->frame->peekAtFront(); + if (header->getReceiverAddress() == peer && header->getTid() == tid) + prepared->cancelled = true; + } + } for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { auto edcaf = edca->getEdcaf(static_cast(ac)); auto frames = edcaf->getInProgressFrames(); @@ -684,6 +822,8 @@ void Hcf::invalidateActions(MacAddress peer, Tid tid, bool originator) void Hcf::retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset) { + if (!originator) + clearInterframeHistory(); if (originator) { auto agreement = originatorBlockAckAgreementHandler->detachAgreement(peer, tid); invalidateActions(peer, tid, originator); @@ -778,6 +918,7 @@ void Hcf::actionTerminated(Packet *packet, bool acknowledged) void Hcf::resetPeer(MacAddress peer) { + clearInterframeHistory(); Enter_Method("resetPeer"); if (!originatorBlockAckAgreementHandler) return; @@ -833,6 +974,16 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& { Enter_Method("processLowerFrame(%s)", packet->getName()); take(packet); + if (auto bar = dynamicPtrCast(header)) { + if (!validateBlockAckRequest(packet, bar)) { + interframeHistory = {}; + if (auto time = packet->findTag()) + pendingReceptions.erase(time->getTransmissionId()); + delete packet; + return; + } + } + bool interframe = interframeRadio && receiveInterframe(packet, header); EV_INFO << "Processing lower frame: " << packet->getName() << endl; auto edcaf = edca->getChannelOwner(); auto action = dynamicPtrCast(header); @@ -878,7 +1029,7 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& else if (hcca->isOwning()) throw cRuntimeError("Hcca is unimplemented!"); else if (isForUs(header)) - recipientProcessReceivedFrame(packet, header); + recipientProcessReceivedFrame(packet, header, interframe); else { EV_INFO << "This frame is not for us" << std::endl; PacketDropDetails details; @@ -888,6 +1039,33 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& } } +bool Hcf::validateBlockAckRequest(Packet *packet, const Ptr& bar) const +{ + // IEEE Std 802.11-2024, 9.3.1.7.2: the supported Compressed BAR has a zero Fragment Number. + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed ? compressed->getTidInfo() : -1; + bool valid = basic || compressed; + if (basic) + valid &= basic->getFragmentNumber() == 0 && !basic->getMultiTid() && !basic->getCompressedBitmap() && + basic->getStartingSequenceNumber().isSpecified() && (basic->getReserved() & 3) == 0; + if (compressed) + valid &= compressed->getFragmentNumber() == 0 && !compressed->getMultiTid() && compressed->getCompressedBitmap() && + compressed->getStartingSequenceNumber().isSpecified(); + if (!valid || tid < 0 || tid > 15 || bar->getChunkLength() != LENGTH_BLOCKACKREQ - B(4) || + packet->getDataLength() != LENGTH_BLOCKACKREQ || bar->getTransmitterAddress().isUnspecified() || + bar->getTransmitterAddress().isMulticast() || bar->getReceiverAddress().isUnspecified() || bar->getReceiverAddress().isMulticast()) + return false; + auto agreement = recipientBlockAckAgreementHandler ? + recipientBlockAckAgreementHandler->getAgreement(tid, bar->getTransmitterAddress()) : nullptr; + // ADDBA has no variant field. INET stops before any protocol update for an active conflicting profile. + if (bar->getReceiverAddress() == mac->getAddress() && agreement && bool(compressed) != (blockAckVariant == BlockAckVariant::COMPRESSED)) + throw cRuntimeError("Block Ack profile conflict at %s: peer=%s TID=%d local=%s received=%s; ADDBA does not negotiate the profile. Configure matching blockAckVariant values and start a new run", + getFullPath().c_str(), bar->getTransmitterAddress().str().c_str(), tid, + blockAckVariant == BlockAckVariant::COMPRESSED ? "compressed" : "basic", compressed ? "compressed" : "basic"); + return true; +} + void Hcf::channelGranted(IChannelAccess *channelAccess) { Enter_Method("channelGranted"); @@ -920,7 +1098,8 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) FrameSequenceContext *Hcf::buildContext(AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); - auto qosContext = new QoSContext(originatorAckPolicy, originatorBlockAckProcedure, originatorBlockAckAgreementHandler, edcaf->getTxopProcedure()); + auto qosContext = new QoSContext(originatorAckPolicy, originatorBlockAckProcedure, originatorBlockAckAgreementHandler, edcaf->getTxopProcedure(), blockAckVariant); + qosContext->prepareData = [this](FrameSequenceContext *context) { return prepareDataTransmission(context); }; return new FrameSequenceContext(mac->getAddress(), modeSet, edcaf->getInProgressFrames(), rtsProcedure, rtsPolicy, nullptr, qosContext); } @@ -945,7 +1124,30 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) edcaf->requestChannel(this); continue; } - Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); + auto frames = edcaf->getInProgressFrames(); + Packet *internallyCollidedFrame = nullptr; + bool scopedQueuedData = false; + if (blockAckVariant == BlockAckVariant::COMPRESSED && originatorBlockAckAgreementHandler) { + auto candidate = frames->peekFrameToTransmit(); + auto data = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + auto agreement = data ? originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()) : nullptr; + if (data && data->getType() == ST_DATA_WITH_QOS && !data->getReceiverAddress().isMulticast() && + agreement && agreement->getIsAddbaResponseReceived() && !frames->containsFrame(candidate)) { + scopedQueuedData = true; + // Collision recovery needs a numbered frame, but it must not form an unadmitted aggregate. + auto service = check_and_cast(originatorDataService); + auto prepared = service->prepareFrame(edcaf->getPendingQueue(), false); + if (prepared) { + auto selected = prepared->packet->peekAtFront(); + if (selected->getReceiverAddress() == data->getReceiverAddress() && selected->getTid() == data->getTid()) + internallyCollidedFrame = service->commitPreparedFrame(*prepared); + } + if (internallyCollidedFrame) + frames->adoptPreparedFrame(internallyCollidedFrame); + } + } + if (!internallyCollidedFrame && !scopedQueuedData) + internallyCollidedFrame = frames->getFrameToTransmit(); if (internallyCollidedFrame == nullptr) continue; auto internallyCollidedHeader = internallyCollidedFrame->peekAtFront(); @@ -1028,7 +1230,7 @@ void Hcf::frameSequenceFinished() finishingSequence = false; } -void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header) +void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe) { EV_INFO << "Processing received frame " << packet->getName() << " as recipient.\n"; emit(packetReceivedFromPeerSignal, packet); @@ -1036,6 +1238,17 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { + auto agreement = data->getType() == ST_DATA_WITH_QOS ? + recipientBlockAckAgreementHandler->getAgreement(data->getTid(), data->getTransmitterAddress()) : nullptr; + if (agreement && (data->getTid() < 0 || data->getTid() > 15 || !data->getSequenceNumber().isSpecified())) { + delete packet; + return; + } + if (agreement && blockAckVariant == BlockAckVariant::COMPRESSED && + (data->getFragmentNumber() != 0 || data->getMoreFragments())) { + delete packet; + return; + } if (data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == BLOCK_ACK && (data->getTid() < 0 || data->getTid() > 15 || !data->getSequenceNumber().isSpecified() || data->getTransmitterAddress().isUnspecified() || data->getTransmitterAddress().isMulticast())) { @@ -1052,16 +1265,17 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { - if (bar->getFragmentNumber() != 0 || bar->getMultiTid() || bar->getCompressedBitmap() || - bar->getTidInfo() < 0 || bar->getTidInfo() > 15 || !bar->getStartingSequenceNumber().isSpecified() || - bar->getTransmitterAddress().isUnspecified() || bar->getTransmitterAddress().isMulticast()) { + else if (auto bar = dynamicPtrCast(header)) { + if (!validateBlockAckRequest(packet, bar)) { delete packet; return; } - auto agreement = recipientBlockAckAgreementHandler->getAgreement(bar->getTidInfo(), bar->getTransmitterAddress()); + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = recipientBlockAckAgreementHandler->getAgreement(tid, bar->getTransmitterAddress()); if (!agreement) { - beginTeardown(bar->getTransmitterAddress(), bar->getTidInfo(), false, 38); + beginTeardown(bar->getTransmitterAddress(), tid, false, 38); delete packet; return; } @@ -1074,7 +1288,7 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { if (dataHeader->getType() == ST_DATA_WITH_QOS && recipientBlockAckAgreementHandler) recipientBlockAckAgreementHandler->qosFrameReceived(dataHeader, this); - sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler)); + sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler, interframe)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { auto accepted = recipientDataService->managementFrameReceived(packet, mgmtHeader); @@ -1102,7 +1316,7 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); - else if (auto blockAckRequest = dynamicPtrCast(header)) { + else if (auto blockAckRequest = dynamicPtrCast(header)) { if (recipientBlockAckProcedure) recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckRequest, recipientAckPolicy, recipientBlockAckAgreementHandler, this); } @@ -1138,6 +1352,10 @@ bool Hcf::dispatchAgreementAction(const Ptr& header) } recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this); auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + if (!previous && agreement) + agreement->setVariant(blockAckVariant); + if (!previous && agreement) + clearInterframeHistory(); if (!previous && agreement) emit(blockAckAgreementAddedSignal, agreement); scheduleInactivityTimer(); @@ -1191,6 +1409,7 @@ bool Hcf::dispatchAgreementAction(const Ptr& header) void Hcf::transmissionComplete(Packet *packet, const Ptr& header) { Enter_Method("transmissionComplete"); + completedTransmissionWasAck = header->getType() == ST_ACK; auto edcaf = edca->getChannelOwner(); if (edcaf) { auto step = check_and_cast(frameSequenceHandler->getContext()->getLastStep()); @@ -1211,6 +1430,15 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) Enter_Method("originatorProcessRtsProtectionFailed"); auto protectedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); + auto context = frameSequenceHandler->getContext(); + auto prepared = context->getQoSContext()->preparedData.get(); + if (prepared && prepared->frame == packet && prepared->preparation) { + // IEEE Std 802.11-2024, 10.23.2.2 and 10.23.2.12.1: BA RTS failure updates QSRC/CW. + // The uncommitted sources retain their lifetimes. No provisional sequence owns an SRC entry. + if (validatePreparedData(*prepared, simTime())) + edcaf->getRecoveryProcedure()->controlFrameTransmissionFailed(); + return; + } if (obsoleteActions.count(packet->getId()) || !edcaf->getInProgressFrames()->containsFrame(packet)) return; if (edcaf) { @@ -1339,9 +1567,12 @@ void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& controlHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + auto basic = dynamicPtrCast(blockAckReq); + auto compressed = dynamicPtrCast(blockAckReq); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); auto agreement = originatorBlockAckAgreementHandler ? - originatorBlockAckAgreementHandler->getAgreement(blockAckReq->getReceiverAddress(), blockAckReq->getTidInfo()) : nullptr; + originatorBlockAckAgreementHandler->getAgreement(blockAckReq->getReceiverAddress(), tid) : nullptr; if (agreement) edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq, agreement); } @@ -1489,13 +1720,18 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { - if (!originatorBlockAckAgreementHandler || - !originatorBlockAckAgreementHandler->getAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo())) + else if (auto blockAck = dynamicPtrCast(header)) { + auto basic = dynamicPtrCast(blockAck); + auto compressed = dynamicPtrCast(blockAck); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(blockAck->getTransmitterAddress(), tid) : nullptr; + if (!agreement) return; - EV_INFO << "BasicBlockAck has arrived" << std::endl; + EV_INFO << "BlockAck has arrived" << std::endl; edcaf->getRecoveryProcedure()->blockAckFrameReceived(); - auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); + auto ackedSeqAndFragNums = compressed ? edcaf->getAckHandler()->processReceivedCompressedBlockAck(compressed, agreement) : + edcaf->getAckHandler()->processReceivedBlockAck(blockAck); if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); EV_TRACE << "It has acknowledged the following frames:" << std::endl; @@ -1522,7 +1758,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetType() == ST_DATA_WITH_QOS) ; // void - else if (dynamicPtrCast(header)) + else if (dynamicPtrCast(header)) ; // void else throw cRuntimeError("Unknown control frame"); @@ -1623,8 +1859,9 @@ simtime_t Hcf::estimateZeroLimitExchange(Packet *packet, const Ptr(header)) - return ppduDuration + sifs + rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + if (auto bar = dynamicPtrCast(header)) + return ppduDuration + sifs + rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration( + dynamicPtrCast(bar) ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); if (auto dataOrMgmt = dynamicPtrCast(header)) { if (recipientAckPolicy->isAckNeeded(dataOrMgmt)) return ppduDuration + sifs + rateSelection->computeResponseAckFrameMode(packet, dataOrMgmt, ResponseQueryRole::PREDICT)->getDuration(LENGTH_ACK); @@ -1639,6 +1876,180 @@ simtime_t Hcf::computeImmediateResponseDuration(Packet *packet, const PtrcomputeResponseAckFrameMode(packet, header, ResponseQueryRole::PREDICT)->getDuration(LENGTH_ACK) : SIMTIME_ZERO; } +bool Hcf::isInitialDataException(const FrameSequenceContext *context, const PreparedDataTransmission& prepared) const +{ + auto header = prepared.frame->peekAtFront(); + // IEEE Std 802.11-2024, 10.23.2.9: the same-size single-MPDU retry exception also permits an A-MSDU. + // Committed Compressed MPDUs retain their payload and length throughout retry; this path forms no A-MPDU. + bool committedRetry = !prepared.preparation && header->getRetry(); + if (!committedRetry && (header->getAMsduPresent() || header->getRetry())) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto step = dynamic_cast(context->getStep(i)); + if (step && step->getPpduEnd() >= SIMTIME_ZERO && + dynamicPtrCast(step->getFrameToTransmit()->peekAtFront())) + return false; + } + return true; +} + +simtime_t Hcf::computePreparedDataCost(const PreparedDataTransmission& prepared, bool protect) const +{ + auto sifs = modeSet->getSifsTime(); + auto cost = prepared.dataMode->getDuration(prepared.frame->getDataLength()); + if (prepared.ackMode) + cost += sifs + prepared.ackMode->getDuration(LENGTH_ACK); + if (protect) + cost += prepared.rtsMode->getDuration(prepared.rtsPrediction->getDataLength()) + + sifs + prepared.ctsMode->getDuration(LENGTH_CTS) + sifs; + return cost; +} + +bool Hcf::canAdmitPreparedData(const FrameSequenceContext *context, const PreparedDataTransmission& prepared, + simtime_t ifs, bool protect) const +{ + auto txop = context->getQoSContext()->txopProcedure; + auto cost = computePreparedDataCost(prepared, protect); + int previousData = 0; + for (int i = 0; i < context->getNumSteps(); i++) { + auto step = dynamic_cast(context->getStep(i)); + if (step && step->getPpduEnd() >= SIMTIME_ZERO && + dynamicPtrCast(step->getFrameToTransmit()->peekAtFront())) + previousData++; + } + // IEEE Std 802.11-2024, 10.23.2.9: zero permits one SU DATA PPDU, including an A-MSDU. + if (txop->getLimit() == 0) { + if (previousData != 0) + return false; + } + else if (ifs + cost > txop->getRemaining() && !isInitialDataException(context, prepared)) + return false; + bool later = context->getNumSteps() > 0; + // The current TransmitStep is already present at the final submission check. + if (later) { + auto last = dynamic_cast(context->getLastStep()); + later = !last || last->getPpduEnd() >= SIMTIME_ZERO || context->getNumSteps() > 1; + } + // 10.23.2.8 samples TXNAV at exchange completion, before the leading IFS. + if (later && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()) + return false; + if (later && protect && txop->getLimit() > 0 && txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION && + (simTime() + ifs - txop->getStart() - txop->getLimit()).raw() % SimTime(1, SIMTIME_US).raw() != 0) + return false; + return true; +} + +bool Hcf::validatePreparedData(const PreparedDataTransmission& prepared, simtime_t start) const +{ + if (prepared.cancelled || prepared.modeSet != modeSet) + return false; + auto header = prepared.frame->peekAtFront(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()) : nullptr; + if (agreement != prepared.agreement || !agreement || !agreement->getIsAddbaResponseReceived() || + !isProfileAllowed(header->getReceiverAddress(), true)) + return false; + auto service = check_and_cast(originatorDataService); + if (prepared.preparation) { + if (!service->isPreparationValid(*prepared.preparation, start)) + return false; + } + else if (!edca->getChannelOwner()->getInProgressFrames()->containsFrame(prepared.frame) || + !isFrameEligible(prepared.frame, true) || edca->getLifetimeHandler()->isExpired(prepared.frame, start)) + return false; + auto peer = mib->findPeerHtState(header->getReceiverAddress()); + if (!isPeerCompatibleMode(modeSet, peer, prepared.dataMode) || + (prepared.rtsMode && !isPeerCompatibleMode(modeSet, peer, prepared.rtsMode))) + return false; + if (originatorAckPolicy->computeAckPolicy(prepared.frame, header, agreement) != header->getAckPolicy()) + return false; + if (prepared.ackMode && (!modeSet->containsMode(prepared.ackMode) || + rateSelection->computeResponseAckFrameMode(prepared.frame, header, ResponseQueryRole::PREDICT) != prepared.ackMode)) + return false; + if (prepared.ctsMode && (!modeSet->containsMode(prepared.ctsMode) || + rateSelection->computeResponseCtsFrameMode(prepared.rtsPrediction.get(), + prepared.rtsPrediction->peekAtFront(), ResponseQueryRole::PREDICT) != prepared.ctsMode)) + return false; + return true; +} + +DataPreparationResult Hcf::prepareDataTransmission(FrameSequenceContext *context) +{ + Enter_Method("prepareDataTransmission"); + if (blockAckVariant != BlockAckVariant::COMPRESSED || !originatorBlockAckAgreementHandler) + return DataPreparationResult::LEGACY; + auto frames = context->getInProgressFrames(); + auto candidate = frames->peekFrameToTransmit(); + auto header = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + if (!header || header->getType() != ST_DATA_WITH_QOS || header->getReceiverAddress().isMulticast()) + return DataPreparationResult::LEGACY; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()); + if (!agreement || !agreement->getIsAddbaResponseReceived()) + return DataPreparationResult::LEGACY; + auto prepared = std::make_unique(); + prepared->modeSet = modeSet; + prepared->agreement = agreement; + auto service = check_and_cast(originatorDataService); + if (frames->containsFrame(candidate)) + prepared->frame = candidate; + else { + prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), true); + if (!prepared->preparation) + return DataPreparationResult::DEFERRED; + prepared->frame = prepared->preparation->packet.get(); + } + auto selected = prepared->frame->peekAtFront(); + if (selected->getReceiverAddress() != header->getReceiverAddress() || selected->getTid() != header->getTid()) + return DataPreparationResult::DEFERRED; + auto txop = context->getQoSContext()->txopProcedure; + prepared->dataMode = rateSelection->computeMode(prepared->frame, + prepared->frame->peekAtFront(), txop); + const IIeee80211Mode *selectedRtsMode = nullptr; + for (;;) { + auto packet = prepared->frame; + auto policy = originatorAckPolicy->computeAckPolicy(packet, packet->peekAtFront(), agreement); + auto updated = packet->removeAtFront(); + bool retry = false; + if (!prepared->preparation) { + auto status = edca->getChannelOwner()->getAckHandler()->getQoSDataAckStatus(updated); + retry = status == QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED || + status == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED || status == QosAckHandler::Status::BLOCK_ACK_ARRIVED_UNACKED; + } + // Forwarded MSDUs can carry the preceding hop's Retry bit. Local ACK state owns this hop's retry decision. + updated->setRetry(retry); + updated->setAckPolicy(policy); + packet->insertAtFront(updated); + setFrameMode(packet, updated, prepared->dataMode); + prepared->ackMode = recipientAckPolicy->isAckNeeded(updated) ? + rateSelection->computeResponseAckFrameMode(packet, updated, ResponseQueryRole::PREDICT) : nullptr; + bool protect = rtsPolicy->isRtsNeeded(packet, updated); + prepared->setRtsPrediction(nullptr); + prepared->rtsMode = nullptr; + prepared->ctsMode = nullptr; + if (protect) { + prepared->setRtsPrediction(new Packet("prepared-RTS", rtsProcedure->buildRtsFrame(updated))); + prepared->rtsPrediction->insertAtBack(makeShared()); + auto rts = prepared->rtsPrediction->peekAtFront(); + if (!selectedRtsMode) + selectedRtsMode = rateSelection->computeMode(prepared->rtsPrediction.get(), rts, txop); + prepared->rtsMode = selectedRtsMode; + setFrameMode(prepared->rtsPrediction.get(), rts, selectedRtsMode); + prepared->ctsMode = rateSelection->computeResponseCtsFrameMode(prepared->rtsPrediction.get(), rts, ResponseQueryRole::PREDICT); + } + if (validatePreparedData(*prepared, simTime() + context->getIfs()) && + canAdmitPreparedData(context, *prepared, context->getIfs(), protect)) { + context->getQoSContext()->preparedData = std::move(prepared); + return DataPreparationResult::READY; + } + if (!prepared->preparation || !prepared->preparation->aggregate) + return DataPreparationResult::DEFERRED; + prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), false); + if (!prepared->preparation) + return DataPreparationResult::DEFERRED; + prepared->frame = prepared->preparation->packet.get(); + } +} + simtime_t Hcf::forecastSingleProtectionSuccessor(Packet *packet, const Ptr& header, TxopProcedure *txop, InProgressFrames *frames, simtime_t completionTime) { @@ -1686,7 +2097,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) auto channelOwner = edca->getChannelOwner(); if (channelOwner) { auto header = packet->peekAtFront(); + auto context = frameSequenceHandler->getContext(); + auto prepared = context->getQoSContext()->preparedData.get(); + auto rtsStep = dynamic_cast(context->getLastStep()); + bool preparedStep = prepared && (prepared->frame == packet || + (rtsStep && rtsStep->getProtectedFrame() == prepared->frame)); + if (preparedStep && (!validatePreparedData(*prepared, simTime() + ifs) || + !canAdmitPreparedData(context, *prepared, ifs, rtsStep != nullptr))) + return false; if (dynamicPtrCast(header) && + !(preparedStep && prepared->preparation) && !channelOwner->getInProgressFrames()->containsFrame(packet)) return false; if (!isFrameEligible(packet, true)) @@ -1710,7 +2130,6 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) if (lifetimeHandler->isExpired(data, simTime() + ifs)) return false; auto txop = channelOwner->getTxopProcedure(); - auto context = frameSequenceHandler->getContext(); bool scopedSingle = txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && !par("isBlockAckSupported").boolValue(); Packet *retainedPacket = packet; auto retainedModeTag = scopedSingle ? retainedPacket->findTag() : nullptr; @@ -1723,14 +2142,21 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && receiveStep->getReceivedFrame()->peekAtFront()->getType() == ST_CTS; } - auto mode = dataAfterCts ? packet->getTag()->getMode() : rateSelection->computeMode(packet, header, txop); + auto compressedBar = dynamicPtrCast(header); + auto mode = preparedStep ? (rtsStep ? prepared->rtsMode : prepared->dataMode) : + compressedBar ? rateSelection->computeCompressedBlockAckReqMode(packet, compressedBar, context->getNumSteps() == 1) : + dataAfterCts ? packet->getTag()->getMode() : rateSelection->computeMode(packet, header, txop); setFrameMode(packet, header, mode); - if (auto bar = dynamicPtrCast(header)) { + if (auto bar = dynamicPtrCast(header)) { + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); auto agreement = originatorBlockAckAgreementHandler ? - originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), bar->getTidInfo()) : nullptr; + originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), tid) : nullptr; if (!agreement || !agreement->getIsAddbaResponseReceived()) return false; - auto response = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + auto response = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration( + compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + response; // IEEE Std 802.11-2024, 10.23.2.8 and 10.23.2.9: a later BAR must fit both bounds. if (context->getNumSteps() > 1 && @@ -1738,7 +2164,33 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()))) return false; } - if (scopedSingle) { + if (preparedStep) { + auto response = rtsStep ? prepared->ctsMode->getDuration(LENGTH_CTS) : + prepared->ackMode ? prepared->ackMode->getDuration(LENGTH_ACK) : SIMTIME_ZERO; + simtime_t duration; + if (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION) { + // Reserve this exchange only. A later BAR obtains contention through the existing arbitration path. + duration = encodeDuration(singleProtectionMechanism->computeDurationField(header, response, + rtsStep ? computePreparedDataCost(*prepared, false) : SIMTIME_ZERO, true), true); + } + else { + auto ppduDuration = mode->getDuration(packet->getDataLength()); + auto projectedStart = simTime() + ifs; + auto reservation = std::max(SIMTIME_ZERO, txop->getReservationEnd() - projectedStart); + auto remaining = std::max(SIMTIME_ZERO, txop->getStart() + txop->getLimit() - projectedStart); + auto cost = computePreparedDataCost(*prepared, rtsStep != nullptr); + bool singleDataOverrun = txop->getLimit() > 0 && ifs + cost > txop->getRemaining() && + isInitialDataException(context, *prepared); + // An allowed single-DATA overrun protects its actual exchange beyond the nominal limit. + duration = singleDataOverrun ? computeMultipleProtectionDuration(0, 0, 0, ppduDuration, cost, false) : + computeMultipleProtectionDuration(txop->getLimit(), remaining, reservation, ppduDuration, + txop->getLimit() == 0 ? cost : SIMTIME_ZERO, txop->getReservationEnd() != 0); + } + auto updated = packet->removeAtFront(); + updated->setDurationField(duration); + packet->insertAtFront(updated); + } + else if (scopedSingle) { auto sifs = modeSet->getSifsTime(); auto cost = mode->getDuration(packet->getDataLength()); simtime_t responseDuration = SIMTIME_ZERO; @@ -1823,6 +2275,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) } else throw cRuntimeError("Undefined protection mechanism"); + if (preparedStep && packet == prepared->frame && prepared->preparation) { + auto service = check_and_cast(originatorDataService); + auto committed = service->commitPreparedFrame(*prepared->preparation); + if (!committed) + return false; + ASSERT(committed == packet); + prepared->preparation.reset(); + channelOwner->getInProgressFrames()->adoptPreparedFrame(committed); + header = committed->peekAtFront(); + } RateSelection::emitDatarateSelected(this, header, mode); EV_DEBUG << "Datarate for " << packet->getName() << " is set to " << mode->getDataMode()->getNetBitrate() << ".\n"; tx->transmitFrame(packet, packet->peekAtFront(), ifs, false, this); @@ -1852,7 +2314,7 @@ void Hcf::transmitControlResponseFrame(Packet *responsePacket, const Ptr(receivedHeader)) responseMode = rateSelection->computeResponseCtsFrameMode(receivedPacket, rtsFrame, ResponseQueryRole::RESPOND); - else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) + else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseBlockAckFrameMode(receivedPacket, blockAckReq, ResponseQueryRole::RESPOND); else if (auto dataOrMgmtHeader = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseAckFrameMode(receivedPacket, dataOrMgmtHeader, ResponseQueryRole::RESPOND); @@ -1951,6 +2413,7 @@ bool Hcf::isSentByUs(const Ptr& header) const void Hcf::corruptedFrameReceived() { Enter_Method("corruptedFrameReceived"); + clearInterframeHistory(); if (frameSequenceHandler->isSequenceRunning() && !startRxTimer->isScheduled()) { frameSequenceHandler->handleStartRxTimeout(); } @@ -1960,6 +2423,11 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + if (interframeRadio) { + auto radioModule = check_and_cast(interframeRadio.get()); + for (auto signal : {receptionStartedSignal, receptionEndedSignal, transmissionStartedSignal, transmissionEndedSignal, IRadio::receptionStateChangedSignal}) + radioModule->unsubscribe(signal, this); + } cancelAndDelete(addbaResponseTimer); cancelAndDelete(dataLifetimeTimer); cancelAndDelete(startRxTimer); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 25fa8a0bb90..11f2b432971 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,6 +8,8 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#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" @@ -37,6 +39,8 @@ #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/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h" namespace inet { namespace ieee80211 { @@ -51,6 +55,31 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: { private: BlockAckVariant blockAckVariant = BlockAckVariant::BASIC; + bool validateBlockAckRequest(Packet *packet, const Ptr& bar) const; + struct InterframeHistory { + physicallayer::TransmissionTimingSnapshot timing; + bool mpdu = false; + bool ack = false; + bool ht = false; + bool noSifsResponse = false; + MacAddress receiver; + std::optional extension; + }; + struct PendingReception { + bool completed = false; + bool sifs = false; + bool rifs = false; + MacAddress precedingReceiver; + physicallayer::TransmissionTimingSnapshot timing; + bool ht = false; + std::optional extension; + }; + opp_component_ptr interframeRadio; + InterframeHistory interframeHistory; + std::map pendingReceptions; + bool completedTransmissionWasAck = false; + void clearInterframeHistory(); + bool receiveInterframe(Packet *packet, const Ptr& header); public: static simsignal_t edcaCollisionDetectedSignal; @@ -130,6 +159,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void handleMessage(cMessage *msg) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, bool value, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); @@ -140,6 +170,12 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: FrameSequenceContext *buildContext(AccessCategory ac); static simtime_t encodeDuration(simtime_t duration, bool holder); simtime_t computeImmediateResponseDuration(Packet *packet, const Ptr& header); + DataPreparationResult prepareDataTransmission(FrameSequenceContext *context); + bool validatePreparedData(const PreparedDataTransmission& prepared, simtime_t start) const; + simtime_t computePreparedDataCost(const PreparedDataTransmission& prepared, bool protect) const; + bool canAdmitPreparedData(const FrameSequenceContext *context, const PreparedDataTransmission& prepared, + simtime_t ifs, bool protect) const; + bool isInitialDataException(const FrameSequenceContext *context, const PreparedDataTransmission& prepared) const; simtime_t forecastSingleProtectionSuccessor(Packet *packet, const Ptr& header, TxopProcedure *txop, InProgressFrames *frames, simtime_t completionTime); static simtime_t computeMultipleProtectionDuration(simtime_t limit, simtime_t remaining, @@ -173,7 +209,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: bool isProfileAllowed(MacAddress peer, bool originator) const; // Recipient - virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header); + virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe = false); virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); virtual void recipientProcessReceivedManagementFrame(const Ptr& header); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 2036bbbe680..e38a6636622 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -29,7 +29,7 @@ module Hcf extends Module like IHcf { parameters: bool isBlockAckSupported = default(false); - string blockAckVariant = default("basic"); // "basic" selects historical compatibility. "compressed" is reserved until its exchange path exists. + string blockAckVariant = default("basic"); // Compressed requires usable HT state. Select Basic explicitly for historical compatibility. string rxModule; string txModule; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc index d5a8ac4d2c3..f392d6fc3aa 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc @@ -12,6 +12,21 @@ namespace ieee80211 { using namespace inet::physicallayer; +PreparedDataTransmission::~PreparedDataTransmission() +{ + if (rtsPrediction) + drop(rtsPrediction.get()); +} + +void PreparedDataTransmission::setRtsPrediction(Packet *packet) +{ + if (rtsPrediction) + drop(rtsPrediction.get()); + rtsPrediction.reset(packet); + if (rtsPrediction) + take(rtsPrediction.get()); +} + FrameSequenceContext::FrameSequenceContext(MacAddress address, Ieee80211ModeSet *modeSet, InProgressFrames *inProgressFrames, IRtsProcedure *rtsProcedure, IRtsPolicy *rtsPolicy, NonQoSContext *nonQoSContext, QoSContext *qosContext) : address(address), modeSet(modeSet), @@ -31,6 +46,11 @@ void FrameSequenceContext::removeLastStep() delete step; } +Packet *FrameSequenceContext::getDataFrameToTransmit() const +{ + return qosContext && qosContext->preparedData ? qosContext->preparedData->frame : inProgressFrames->getFrameToTransmit(); +} + simtime_t FrameSequenceContext::getIfs() const { return getNumSteps() == 0 ? 0 : modeSet->getSifsTime(); // TODO pifs diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h index ba9a3695257..85fa551c5f4 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h @@ -8,13 +8,18 @@ #ifndef __INET_FRAMESEQUENCECONTEXT_H #define __INET_FRAMESEQUENCECONTEXT_H +#include +#include + #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" #include "inet/linklayer/ieee80211/mac/contract/IFrameSequence.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorAckPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/contract/IRtsPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/RtsProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" @@ -24,20 +29,46 @@ namespace inet { namespace ieee80211 { +class FrameSequenceContext; + +enum class DataPreparationResult { LEGACY, READY, DEFERRED }; + +/** Retains one actual mode decision until DATA submission or cancellation. */ +struct INET_API PreparedDataTransmission : public cObject +{ + std::unique_ptr preparation; + Packet *frame = nullptr; // Borrowed from preparation, or from InProgressFrames after commitment. + const physicallayer::Ieee80211ModeSet *modeSet = nullptr; + const physicallayer::IIeee80211Mode *dataMode = nullptr; + const physicallayer::IIeee80211Mode *rtsMode = nullptr; + const physicallayer::IIeee80211Mode *ackMode = nullptr; + const physicallayer::IIeee80211Mode *ctsMode = nullptr; + std::unique_ptr rtsPrediction; + OriginatorBlockAckAgreement *agreement = nullptr; // Identity only; validation obtains current state from its owner. + bool cancelled = false; + + ~PreparedDataTransmission() override; + void setRtsPrediction(Packet *packet); +}; + class INET_API QoSContext { public: - QoSContext(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler, TxopProcedure *txopProcedure) : + QoSContext(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler, TxopProcedure *txopProcedure, BlockAckVariant variant = BlockAckVariant::BASIC) : ackPolicy(ackPolicy), blockAckProcedure(blockAckProcedure), blockAckAgreementHandler(blockAckAgreementHandler), - txopProcedure(txopProcedure) + txopProcedure(txopProcedure), + blockAckVariant(variant) {} IOriginatorQoSAckPolicy *ackPolicy = nullptr; IOriginatorBlockAckProcedure *blockAckProcedure = nullptr; IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler = nullptr; TxopProcedure *txopProcedure = nullptr; + const BlockAckVariant blockAckVariant; + std::function prepareData; + std::unique_ptr preparedData; }; class INET_API NonQoSContext @@ -79,6 +110,8 @@ class INET_API FrameSequenceContext : public cObject virtual IFrameSequenceStep *getStepBeforeLast() const { return steps.size() > 1 ? steps[steps.size() - 2] : nullptr; } virtual InProgressFrames *getInProgressFrames() const { return inProgressFrames; } + // A reversible preview replaces destructive extraction only for the scoped QoS path. + Packet *getDataFrameToTransmit() const; virtual IRtsProcedure *getRtsProcedure() const { return rtsProcedure; } virtual IRtsPolicy *getRtsPolicy() const { return rtsPolicy; } diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index 7a49e0ece7c..7bcd7d3be5c 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -64,6 +64,14 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte auto frames = context->getInProgressFrames(); auto qos = context->getQoSContext(); bool bar = qos->ackPolicy->isBlockAckReqNeeded(frames, qos->txopProcedure, qos->blockAckAgreementHandler); + qos->preparedData.reset(); + if (!bar && qos->prepareData) { + auto preparation = qos->prepareData(context); + if (preparation == DataPreparationResult::READY) + return true; + if (preparation == DataPreparationResult::DEFERRED) + return false; + } bool hasFrameToTransmit = qos->blockAckAgreementHandler ? bar || frames->peekFrameToTransmit() != nullptr : frames->hasInProgressFrames(); if (hasFrameToTransmit) { @@ -101,7 +109,7 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte bool HcfFs::hasMoreTxOpsAndMulticast(RepeatingFs *frameSequence, FrameSequenceContext *context) { - return hasMoreTxOps(frameSequence, context) && context->getInProgressFrames()->getFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); + return hasMoreTxOps(frameSequence, context) && context->getDataFrameToTransmit()->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 f9a3de4a6e5..66da226a753 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -40,7 +40,7 @@ IFrameSequenceStep *RtsFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto dataOrMgmtPacket = context->getInProgressFrames()->getFrameToTransmit(); + auto dataOrMgmtPacket = context->getDataFrameToTransmit(); auto rtsFrame = context->getRtsProcedure()->buildRtsFrame(dataOrMgmtPacket->peekAtFront()); auto rtsPacket = new Packet("RTS"); rtsPacket->insertAtBack(rtsFrame); @@ -119,7 +119,7 @@ IFrameSequenceStep *DataFs::prepareStep(FrameSequenceContext *context) // RTS/CTS retains its selected DATA even if its lifetime expires during protection. // HCF rejects the next submission before Tx accepts that expired DATA. if (packet == nullptr) - packet = context->getInProgressFrames()->getFrameToTransmit(); + packet = context->getDataFrameToTransmit(); return new TransmitStep(packet, context->getIfs()); } case 1: @@ -275,7 +275,7 @@ IFrameSequenceStep *RtsCtsFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto packet = context->getInProgressFrames()->getFrameToTransmit(); + auto packet = context->getDataFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); auto rtsFrame = context->getRtsProcedure()->buildRtsFrame(dataOrMgmtHeader); auto rtsPacket = new Packet("RTS"); @@ -417,8 +417,10 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); - auto blockAckReq = context->getQoSContext()->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); - auto blockAckPacket = new Packet("BasicBlockAckReq", blockAckReq); + bool compressed = qos->blockAckVariant == BlockAckVariant::COMPRESSED; + auto blockAckReq = compressed ? qos->blockAckProcedure->buildCompressedBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber) : + qos->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); + auto blockAckPacket = new Packet(compressed ? "CompressedBlockAckReq" : "BasicBlockAckReq", blockAckReq); blockAckPacket->insertAtBack(makeShared()); return new TransmitStep(blockAckPacket, context->getIfs(), true); } @@ -446,12 +448,33 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) step++; auto receivedPacket = receiveStep->getReceivedFrame(); const auto& receivedHeader = receivedPacket->peekAtFront(); - auto response = dynamicPtrCast(receivedHeader); auto requestStep = check_and_cast(context->getStep(firstStep)); - auto request = requestStep->getFrameToTransmit()->peekAtFront(); - return context->isForUs(receivedHeader) && response && - response->getTransmitterAddress() == request->getReceiverAddress() && - response->getTidInfo() == request->getTidInfo(); + auto request = requestStep->getFrameToTransmit()->peekAtFront(); + if (auto basic = dynamicPtrCast(request)) { + auto response = dynamicPtrCast(receivedHeader); + bool valid = response && !response->getMultiTid() && !response->getCompressedBitmap() && + response->getFragmentNumber() == 0 && response->getStartingSequenceNumber().isSpecified() && + (response->getReserved() & 3) == 0 && response->getChunkLength() == LENGTH_BASIC_BLOCKACK - B(4) && + receivedPacket->getDataLength() == LENGTH_BASIC_BLOCKACK && response->getBlockAckBitmapArraySize() == 64; + if (valid) + for (int i = 0; i < 64; i++) + valid &= response->getBlockAckBitmap(i).getSize() == 16; + return valid && context->isForUs(receivedHeader) && + response->getReceiverAddress() == basic->getTransmitterAddress() && + response->getTransmitterAddress() == basic->getReceiverAddress() && + response->getTidInfo() == basic->getTidInfo(); + } + auto compressed = dynamicPtrCast(request); + auto response = dynamicPtrCast(receivedHeader); + // IEEE Std 802.11-2024, 9.3.1.8.2: this profile supports the 64-bit bitmap without dynamic fragmentation. + return context->isForUs(receivedHeader) && compressed && response && + response->getReceiverAddress() == compressed->getTransmitterAddress() && + response->getTransmitterAddress() == compressed->getReceiverAddress() && + response->getTidInfo() == compressed->getTidInfo() && + !response->getMultiTid() && response->getCompressedBitmap() && response->getFragmentNumber() == 0 && + response->getStartingSequenceNumber().isSpecified() && response->getBlockAckBitmap().getSize() == 64 && + response->getChunkLength() == LENGTH_COMPRESSED_BLOCKACK - B(4) && + receivedPacket->getDataLength() == LENGTH_COMPRESSED_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 e0daefabaa7..d2986e6dffe 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 qos = context->getQoSContext(); if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) return 2; - auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + auto frameToTransmit = context->getDataFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); if (dynamicPtrCast(macHeader)) return 3; @@ -71,7 +71,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte bool TxOpFs::isRtsCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *context) { - auto protectedFrame = context->getInProgressFrames()->getFrameToTransmit(); + auto protectedFrame = context->getDataFrameToTransmit(); return context->getRtsPolicy()->isRtsNeeded(protectedFrame, protectedFrame->peekAtFront()); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index 11ec3cb722d..80d2478f42d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -311,7 +311,7 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu // if (msduIntegrityAndProtection) // frame = protectMsduIfNeeded(frame); std::vector *fragments = nullptr; - if (fragmentationPolicy) + if (fragmentationPolicy && (!fragmentationPermitted || fragmentationPermitted(packet))) fragments = fragmentIfNeeded(packet); if (!fragments) fragments = new std::vector({ packet }); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 5f613686c90..6e139a402fc 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -43,6 +43,7 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService std::function expireFrames; std::function extractionEligible; std::function aMsduPermitted; + std::function fragmentationPermitted; int64_t nextDataAllocationIndex = 0; protected: @@ -54,6 +55,7 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); public: + void setFragmentationPermission(std::function permission) { fragmentationPermitted = permission; } void setEligibility(std::function eligible, std::function permission) { extractionEligible = eligible; aMsduPermitted = permission; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index c79877d4a72..e213b5665bb 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -109,6 +109,33 @@ void QosAckHandler::dropFrames(std::set>> QosAckHandler::processReceivedCompressedBlockAck( + const Ptr& blockAck, const OriginatorBlockAckAgreement *agreement) +{ + std::set acked; + if (!agreement) + return acked; + auto start = blockAck->getStartingSequenceNumber(); + const auto& bitmap = blockAck->getBlockAckBitmap(); + // IEEE Std 802.11-2024, 10.25.6.8: positive bits cover transmitted frames throughout the active window. + for (auto& entry : ackStatuses) { + if (entry.first.first != blockAck->getTransmitterAddress() || entry.first.second.first != blockAck->getTidInfo()) + continue; + auto sequence = SequenceNumberCyclic(entry.first.second.second.getSequenceNumber()); + auto& status = entry.second; + bool transmitted = status != Status::FRAME_NOT_YET_TRANSMITTED && status != Status::NO_ACK_REQUIRED; + int offset = OriginatorBlockAckAgreement::computeSequenceDistance(start, sequence); + if (transmitted && agreement->containsSequence(sequence) && entry.first.second.second.getFragmentNumber() == 0 && + offset < 64 && bitmap.getBit(offset)) { + status = Status::BLOCK_ACK_ARRIVED_ACKED; + acked.insert(entry.first); + } + else if (status == Status::WAITING_FOR_BLOCK_ACK) + status = Status::BLOCK_ACK_ARRIVED_UNACKED; + } + return acked; +} + std::set>> QosAckHandler::processReceivedBlockAck(const Ptr& blockAck) { MacAddress receiverAddr = blockAck->getTransmitterAddress(); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 96f1026d85f..69371236796 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -57,6 +57,8 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void processReceivedAck(const Ptr& ack, const Ptr& ackedHeader); virtual std::set>> processReceivedBlockAck(const Ptr& blockAck); + std::set>> processReceivedCompressedBlockAck( + const Ptr& blockAck, const OriginatorBlockAckAgreement *agreement); virtual void processFailedBlockAckReq(const Ptr& blockAckReq); virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc index fbd4606601a..e2f2b63c464 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc @@ -67,8 +67,9 @@ simtime_t SingleProtectionMechanism::computeCtsDurationField(const Ptr& blockAckReq) { // TODO ACK or BlockAck frame, as applicable - if (dynamicPtrCast(blockAckReq)) { - simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + if (dynamicPtrCast(blockAckReq) || dynamicPtrCast(blockAckReq)) { + simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::PREDICT)->getDuration( + dynamicPtrCast(blockAckReq) ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); simtime_t blockAckReqDurationPerId = blockAckFrameDuration + modeSet->getSifsTime(); return blockAckReqDurationPerId; } diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 416e9709ad3..190afe1d5c6 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -87,6 +87,16 @@ void InProgressFrames::ensureHasFrameToTransmit() } } +void InProgressFrames::adoptPreparedFrame(Packet *packet) +{ + Enter_Method("adoptPreparedFrame"); + take(packet); + ackHandler->frameGotInProgress(packet->peekAtFront()); + inProgressFrames.push_back(packet); + packet->setArrivalTime(simTime()); + emit(packetEnqueuedSignal, packet); +} + Packet *InProgressFrames::getFrameToTransmit() { ensureHasFrameToTransmit(); diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 65b13b67aa7..4804e562628 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -52,6 +52,8 @@ class INET_API InProgressFrames : public SimpleModule virtual int getLength() const { return inProgressFrames.size(); } virtual Packet *getFrames(int i) const { return inProgressFrames[i]; } bool containsFrame(const Packet *packet) const; + // Takes ownership after the data service commits a reversible preparation. + void adoptPreparedFrame(Packet *packet); virtual Packet *getFrameToTransmit(); virtual Packet *peekFrameToTransmit(); SequenceNumberCyclic getCandidateSequenceNumber(Packet *packet) const; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 6076a48172d..bd93bd3c52d 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -131,6 +131,17 @@ static const IIeee80211Mode *selectCompatibleMode(const Ieee80211ModeSet *modeSe } // namespace +bool isPeerCompatibleMode(const Ieee80211ModeSet *modeSet, + const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode) +{ + if (!modeSet || !mode || !modeSet->containsMode(mode)) + return false; + if (mode->getHtMcsIndex() < 0) + return true; + return peerHtState && peerHtState->valid && + isCompatibleHtMode(mode, peerHtState->negotiatedCapabilities.localTxPeerRx, peerHtState->negotiatedCapabilities.operation); +} + const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode, const MacAddress& peerAddress) { diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index 5197aa38891..e512fbbdb28 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -15,6 +15,10 @@ namespace inet { namespace ieee80211 { +/** Checks the retained transmit mode without a new rate-control decision. */ +INET_API bool isPeerCompatibleMode(const physicallayer::Ieee80211ModeSet *modeSet, + const Ieee80211Mib::PeerHtState *peerHtState, const physicallayer::IIeee80211Mode *mode); + /** * Selects a mode compatible with local transmission to the peer. * Non-HT modes remain unchanged. Absent peer state selects a legacy mode diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index cb9b112bd42..4712d009d0c 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -61,7 +61,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { - if (dynamicPtrCast(blockAckReq)) { + if (dynamicPtrCast(blockAckReq) || dynamicPtrCast(blockAckReq)) { return agreement != nullptr; // 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. @@ -94,6 +94,13 @@ simtime_t RecipientQosAckPolicy::computeBasicBlockAckDurationField(Packet *packe return basicBlockAckReq->getDurationField() - modeSet->getSifsTime() - computeBasicBlockAckDuration(packet, basicBlockAckReq); } +simtime_t RecipientQosAckPolicy::computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const +{ + // IEEE Std 802.11-2024, 9.2.5.6: subtract the actual response airtime and SIFS. + auto airtime = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::RESPOND)->getDuration(LENGTH_COMPRESSED_BLOCKACK); + return std::max(SIMTIME_ZERO, blockAckReq->getDurationField() - modeSet->getSifsTime() - airtime); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index 9f4ce456784..0012ef77b55 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -34,6 +34,7 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient 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 computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const override; }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index e73a15720c1..e3f9ea2151d 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -53,14 +53,17 @@ Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) return nullptr; } -std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) +std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe) { Enter_Method("dataFrameReceived"); take(dataPacket); if (blockAckAgreementHandler) { auto agreement = blockAckAgreementHandler->getAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); - if (agreement) + if (agreement) { agreement->recordReceived(dataHeader); + if (agreement->getVariant() == BlockAckVariant::COMPRESSED) + agreement->getBlockAckRecord()->compressedFrameReceived(dataHeader); + } } // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { @@ -77,7 +80,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data MacAddress originatorAddr = dataHeader->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader, interframe); } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -137,14 +140,19 @@ std::vector RecipientQosMacDataService::managementFrameReceived(Packet std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReleasedFrames frames; if (blockAckReordering) { - Tid tid = blockAckReq->getTidInfo(); + auto basic = dynamicPtrCast(blockAckReq); + auto compressed = dynamicPtrCast(blockAckReq); + if (!basic && !compressed) + return {}; + Tid tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) + if (agreement) { frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); + } else return std::vector(); } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index 10b26ca843f..066359162b1 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -48,7 +48,7 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, public: virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) override; virtual void clearPeer(MacAddress peer) override; - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; + virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; }; diff --git a/tests/module/Ieee80211BlockAckReordering_1.test b/tests/module/Ieee80211BlockAckReordering_1.test index 449a6041fde..3731b956b54 100644 --- a/tests/module/Ieee80211BlockAckReordering_1.test +++ b/tests/module/Ieee80211BlockAckReordering_1.test @@ -3,9 +3,10 @@ Check S1 recipient delivery through RecipientQosMacDataService. Supply accepted Basic agreements with known buffer sizes. Check fragment capacity, complete gap release, cyclic delivery, and BAR before DATA. Check window movement, incomplete-unit disposal, peer/TID isolation, and A-MSDU order. -The fixture supplies agreements; it does not test ADDBA negotiation or Compressed exchanges. +The fixture supplies agreements; it does not test ADDBA negotiation. +Compressed cases check gap release, cyclic delivery, peer/TID isolation, and receipt retention through the production recipient service. -%extraargs: -r 0..7 +%extraargs: -r 0..10 %file: RecipientReordering.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -40,8 +41,9 @@ Define_Module(ReorderingRecipient); class ReorderingAgreements : public RecipientBlockAckAgreementHandler { public: - void accept(MacAddress peer, Tid tid, int start, int size) { + void accept(MacAddress peer, Tid tid, int start, int size, BlockAckVariant variant = BlockAckVariant::BASIC) { auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), size, SIMTIME_ZERO); + agreement->setVariant(variant); agreement->addbaResposneSent(); blockAckAgreements[{peer, tid}] = agreement; } @@ -224,6 +226,33 @@ class RecipientReorderingTest : public cSimpleModule { checkDelivery(receive(makeData(101, "fragmented-frag1", 1)), {"fragmented", "third"}); checkWindow(103, 0, peer); break; + case 8: + case 9: { + int start = scenario == 8 ? 100 : 4095; + agreements.accept(peer, 5, start, 4, BlockAckVariant::COMPRESSED); + checkDelivery(receive(makeData(start, "first")), {"first"}); + checkDelivery(receive(makeData((start + 2) % 4096, "third")), {}); + checkDelivery(receive(makeData((start + 1) % 4096, "second")), {"second", "third"}); + checkWindow((start + 3) % 4096, 0, peer); + auto record = agreements.getAgreement(5, peer)->getBlockAckRecord(); + for (int offset = 0; offset < 4; offset++) + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start) + offset) == (offset < 3)); + break; + } + case 10: + agreements.accept(peer, 5, 100, 4, BlockAckVariant::COMPRESSED); + agreements.accept(otherPeer, 5, 100, 4, BlockAckVariant::COMPRESSED); + agreements.accept(peer, 6, 100, 4, BlockAckVariant::COMPRESSED); + checkDelivery(receive(makeData(101, "peer-one")), {}); + checkDelivery(receive(makeData(101, "peer-two", 0, false, otherPeer)), {}); + checkDelivery(receive(makeData(101, "tid-six", 0, false, peer, 6)), {}); + checkDelivery(receive(makeData(100, "first")), {"first", "peer-one"}); + checkWindow(102, 0, peer); + checkWindow(100, 1, otherPeer); + checkWindow(100, 1, peer, 6); + ASSERT(!agreements.getAgreement(5, otherPeer)->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + ASSERT(!agreements.getAgreement(6, peer)->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + break; default: throw cRuntimeError("Unknown recipient scenario"); } @@ -267,7 +296,7 @@ seed-set = 0 cmdenv-express-mode = true record-vector-results = false record-scalar-results = false -*.test.scenario = ${scenario=0..7} +*.test.scenario = ${scenario=0..10} %contains: stdout Recipient reordering checks passed. diff --git a/tests/module/Ieee80211BlockAckScheduling_1.test b/tests/module/Ieee80211BlockAckScheduling_1.test index 3fe472f6f68..ec072ea0dd2 100644 --- a/tests/module/Ieee80211BlockAckScheduling_1.test +++ b/tests/module/Ieee80211BlockAckScheduling_1.test @@ -1,11 +1,12 @@ %description: -Exercise Basic BAR selection and final DELBA through HCF, frame sequences, EDCA, and Tx. +Exercise Basic and Compressed BAR selection and final DELBA through HCF, frame sequences, EDCA, and Tx. The fixture supplies accepted agreements and injects responses at the HCF receive boundary. +Compressed cases supply peer HT capabilities; the fixture does not test capability negotiation. Response events include SIFS and the selected response airtime. Cases cover short bursts, windows smaller than the threshold, two peers, BAR timeout, negative bitmap bits, Normal Ack window progress, and late BAR admission. -%extraargs: -r 0..29 +%extraargs: -r 0..75 %file: Scheduling.cc #include @@ -17,13 +18,16 @@ negative bitmap bits, Normal Ack window progress, and late BAR admission. #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class SchedulingRecipientAgreements : public RecipientBlockAckAgreementHandler { public: - void accept(MacAddress peer) { - auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + void accept(MacAddress peer, BlockAckVariant variant, int start = 0) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, SIMTIME_ZERO); + agreement->setVariant(variant); agreement->addbaResposneSent(); agreement->calculateExpirationTime(); blockAckAgreements[{peer, 5}] = agreement; @@ -33,19 +37,69 @@ class SchedulingRecipient : public Hcf { public: std::vector delivered; bool checkedBitmap = false; + bool orderedReceipt = false; + void publishPrimaryChannel() { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + } + void checkOrderedReceipt() { + Enter_Method("checkOrderedReceipt"); + delete recipientBlockAckAgreementHandler; + auto agreements = new SchedulingRecipientAgreements(); + recipientBlockAckAgreementHandler = agreements; + orderedReceipt = true; + publishPrimaryChannel(); + mib->setPeerHtCapabilities(MacAddress("02:00:00:00:00:01"), mib->localHtCapabilities, mib->getHtOperation()); + agreements->accept(MacAddress("02:00:00:00:00:01"), BlockAckVariant::COMPRESSED, 100); + for (int sequence : {100, 102, 101}) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(data->getTransmitterAddress()); + data->setTid(5); + data->setAckPolicy(BLOCK_ACK); + data->setSequenceNumber(SequenceNumberCyclic(sequence)); + auto packet = new Packet("ordered", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getSlowestMode()); + processLowerFrame(packet, data); + ASSERT((delivered == (sequence == 101 ? std::vector{100, 101, 102} : std::vector{100}))); + } + auto bar = makeShared(); + bar->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + bar->setReceiverAddress(mac->getAddress()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("ordered-bar", bar); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getSlowestMode()); + processLowerFrame(packet, bar); + ASSERT(checkedBitmap); + } void prepare() { Enter_Method("prepare"); delete recipientBlockAckAgreementHandler; auto agreements = new SchedulingRecipientAgreements(); recipientBlockAckAgreementHandler = agreements; - agreements->accept(MacAddress("02:00:00:00:00:01")); + if (std::string(par("blockAckVariant").stringValue()) == "compressed") { + publishPrimaryChannel(); + mib->setPeerHtCapabilities(MacAddress("02:00:00:00:00:01"), mib->localHtCapabilities, mib->getHtOperation()); + } + agreements->accept(MacAddress("02:00:00:00:00:01"), std::string(par("blockAckVariant").stringValue()) == "compressed" ? BlockAckVariant::COMPRESSED : BlockAckVariant::BASIC); subscribe(blockAckAgreementDeletedSignal, this); } void receive(Packet *packet) { Enter_Method("receive"); + take(packet); auto header = packet->peekAtFront(); if (auto data = dynamicPtrCast(header)) { - if (data->getSequenceNumber() == SequenceNumberCyclic(0) && data->getFragmentNumber() != 0) { + if (data->getSequenceNumber() == SequenceNumberCyclic(0) && + (std::string(par("blockAckVariant").stringValue()) == "compressed" || data->getFragmentNumber() != 0)) { delete packet; return; } @@ -80,25 +134,42 @@ class SchedulingRecipient : public Hcf { ASSERT(delivered.empty()); } } + if (auto response = dynamicPtrCast(header)) { + if (orderedReceipt) { + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(100)); + for (int bit = 0; bit < 64; bit++) + ASSERT(response->getBlockAckBitmap().getBit(bit) == (bit < 3)); + ASSERT((delivered == std::vector{100, 101, 102})); + checkedBitmap = true; + delete packet; + return; + } + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(!response->getBlockAckBitmap().getBit(0)); + if (response->getBlockAckBitmap().getBit(1) && response->getBlockAckBitmap().getBit(2)) { + checkedBitmap = true; + ASSERT(delivered.empty()); + } + } delete packet; } }; Define_Module(SchedulingRecipient); class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { public: - void accept(MacAddress peer, int size) { + void accept(MacAddress peer, int size, Tid tid = 5) { auto request = makeShared(); request->setReceiverAddress(peer); - request->setTid(5); + request->setTid(tid); request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setBufferSize(size); request->setBlockAckPolicy(true); createAgreement(request); - auto agreement = getAgreement(peer, 5); + auto agreement = getAgreement(peer, tid); agreement->setIsAddbaResponseReceived(true); agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); agreement->calculateExpirationTime(); - finishProcedure(peer, 5); + finishProcedure(peer, tid); } }; class SchedulingHcf : public Hcf { @@ -111,15 +182,26 @@ class SchedulingHcf : public Hcf { int retries = 0; int dataInTxop = 0; bool responsePending = false; - Ptr pendingBar; + Ptr pendingBar; const physicallayer::IIeee80211Mode *pendingResponseMode = nullptr; bool firstBarWasStandalone = false; int expired = 0; + bool expiredDuringExchange = false; int collisions = 0; SchedulingRecipient *recipient = nullptr; simtime_t responseEnd; simtime_t txopDeadline; int scenario() const { return par("scenario"); } + bool compressed() const { return std::string(par("blockAckVariant").stringValue()) == "compressed"; } + b responseLength() const { return compressed() ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK; } + Tid barTid(const Ptr& bar) const { + return compressed() ? check_and_cast(bar.get())->getTidInfo() : + check_and_cast(bar.get())->getTidInfo(); + } + SequenceNumberCyclic barStart(const Ptr& bar) const { + return compressed() ? check_and_cast(bar.get())->getStartingSequenceNumber() : + check_and_cast(bar.get())->getStartingSequenceNumber(); + } int burst() const { switch (scenario()) { case 0: return 1; @@ -146,7 +228,17 @@ class SchedulingHcf : public Hcf { delete originatorBlockAckAgreementHandler; auto agreements = new SchedulingAgreements(); originatorBlockAckAgreementHandler = agreements; + if (compressed()) { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + mib->setPeerHtCapabilities(other, mib->localHtCapabilities, mib->getHtOperation()); + } agreements->accept(peer, window()); + if (compressed() && scenario() == 0) + check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf"))->checkOrderedReceipt(); if (scenario() == 9) { recipient = check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf")); recipient->prepare(); @@ -154,6 +246,8 @@ class SchedulingHcf : public Hcf { } if (scenario() == 4) agreements->accept(other, 64); + if (scenario() == 18) + agreements->accept(peer, 64, 4); rx->setContentionBlocked(true); for (int i = 0; i < burst(); i++) queueData(peer, scenario() == 9 ? (i == 0 ? 200 : 10) : scenario() == 7 && i == 0 ? 1100 : 100); @@ -173,6 +267,10 @@ class SchedulingHcf : public Hcf { for (int i = 0; i < 8; i++) queueData(other, 100); } + if (scenario() == 18) { + for (int i = 0; i < 8; i++) + queueData(peer, 100, 4); + } rx->setContentionBlocked(false); scheduleAfter(SimTime(40, SIMTIME_MS), new cMessage("verify")); } @@ -180,8 +278,11 @@ class SchedulingHcf : public Hcf { void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { if (signal == packetDroppedSignal) { auto drop = dynamic_cast(details); - if (drop && drop->getReason() == LIFETIME_EXPIRED) + if (drop && drop->getReason() == LIFETIME_EXPIRED) { expired++; + expiredDuringExchange |= frameSequenceHandler->isSequenceRunning() && + originatorBlockAckAgreementHandler->getAgreement(peer, 5) != nullptr; + } if (source == this) return; } @@ -226,7 +327,7 @@ class SchedulingHcf : public Hcf { } bool transmitFrame(Packet *packet, simtime_t ifs) override { auto header = packet->peekAtFront(); - auto bar = dynamicPtrCast(header); + auto bar = dynamicPtrCast(header); if (bar) { auto frames = edca->getChannelOwner()->getInProgressFrames(); auto candidate = frames->peekFrameToTransmit(); @@ -256,6 +357,8 @@ class SchedulingHcf : public Hcf { } if (scenario() == 4 && data->getReceiverAddress() == other) ASSERT(bars >= 1); + if (scenario() == 18 && data->getTid() == 4) + ASSERT(bars >= 1); auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); if ((scenario() == 12 || scenario() == 14) && data->getTid() == 6) ASSERT(!data->getRetry()); @@ -273,13 +376,13 @@ class SchedulingHcf : public Hcf { if (bars == 1) { firstBarWasStandalone = dataInTxop == 0; ASSERT(bar->getReceiverAddress() == peer); - ASSERT(bar->getTidInfo() == 5); + ASSERT(barTid(bar) == 5); if (scenario() != 9) ASSERT(dataCount == (scenario() == 2 ? 1 : scenario() == 3 ? 4 : scenario() == 7 || scenario() == 12 || scenario() == 14 ? 2 : scenario() == 11 ? 5 : burst())); } auto mode = packet->getTag()->getMode(); auto responseMode = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT); - auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_BASIC_BLOCKACK); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + responseMode->getDuration(responseLength()); auto txop = edca->getChannelOwner()->getTxopProcedure(); auto end = simTime() + ifs + cost; if (dataInTxop > 1 && txop->getLimit() > 0) @@ -287,7 +390,7 @@ class SchedulingHcf : public Hcf { std::cout << "BAR timing: scenario=" << scenario() << " multiple=" << par("multiple").boolValue() << " start=" << simTime() << " ifs=" << ifs << " bytes=" << packet->getByteLength() << " rate=" << mode->getDataMode()->getNetBitrate() << " responseRate=" << responseMode->getDataMode()->getNetBitrate() - << " response=" << responseMode->getDuration(LENGTH_BASIC_BLOCKACK) << " end=" << end + << " response=" << responseMode->getDuration(responseLength()) << " end=" << end << " deadline=" << txop->getStart() + txop->getLimit() << " data=" << dataInTxop << "\n"; } if (auto delba = dynamicPtrCast(header)) { @@ -300,7 +403,7 @@ class SchedulingHcf : public Hcf { } void transmissionComplete(Packet *packet, const Ptr& header) override { Enter_Method("transmissionComplete"); - auto bar = dynamicPtrCast(header); + auto bar = dynamicPtrCast(header); auto data = dynamicPtrCast(header); if (recipient && (data || bar || dynamicPtrCast(header))) recipient->receive(packet->dup()); @@ -312,7 +415,7 @@ class SchedulingHcf : public Hcf { pendingResponseMode = bar ? rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT) : rateSelection->computeResponseAckFrameMode(packet, dynamicPtrCast(header), ResponseQueryRole::PREDICT); - auto duration = pendingResponseMode->getDuration(bar ? LENGTH_BASIC_BLOCKACK : LENGTH_ACK); + auto duration = pendingResponseMode->getDuration(bar ? responseLength() : LENGTH_ACK); responseEnd = simTime() + modeSet->getSifsTime() + duration; txopDeadline = edca->getChannelOwner()->getTxopProcedure()->getStart() + edca->getChannelOwner()->getTxopProcedure()->getLimit(); scheduleAt(responseEnd, new cMessage("response")); @@ -327,10 +430,25 @@ class SchedulingHcf : public Hcf { responsePending = false; Ptr header; if (pendingBar) { + if (compressed()) { + auto response = makeShared(); + response->setTransmitterAddress(pendingBar->getReceiverAddress()); + response->setTidInfo(barTid(pendingBar)); + auto responseStart = barStart(pendingBar); + if (bars == 1 && scenario() >= 15 && scenario() <= 17) + responseStart = responseStart + (scenario() == 15 ? -2 : scenario() == 16 ? 1 : 100); + response->setStartingSequenceNumber(responseStart); + BitVector bits; + for (int i = 0; i < 64; i++) + bits.appendBit(scenario() == 9 ? i != 0 : !(bars == 1 && (scenario() == 6 || scenario() == 17))); + response->setBlockAckBitmap(bits); + header = response; + } + else { auto response = makeShared(); response->setTransmitterAddress(pendingBar->getReceiverAddress()); - response->setTidInfo(pendingBar->getTidInfo()); - response->setStartingSequenceNumber(pendingBar->getStartingSequenceNumber()); + response->setTidInfo(barTid(pendingBar)); + response->setStartingSequenceNumber(barStart(pendingBar)); for (int i = 0; i < 64; i++) { BitVector bits; for (int j = 0; j < 16; j++) @@ -338,6 +456,7 @@ class SchedulingHcf : public Hcf { response->setBlockAckBitmap(i, bits); } header = response; + } } else header = makeShared(); @@ -352,14 +471,16 @@ class SchedulingHcf : public Hcf { delete message; std::cout << "Scheduling totals: data=" << dataCount << " bars=" << bars << " delbas=" << delbas << " refused=" << refusedBars << "\n"; ASSERT(bars > 0); - ASSERT(delbas == (scenario() == 4 ? 2 : 1)); + ASSERT(delbas == (scenario() == 4 || scenario() == 18 ? 2 : 1)); if (scenario() != 9) - ASSERT(dataCount == burst() + (scenario() == 4 ? 8 : scenario() == 5 || scenario() == 6 || scenario() == 12 || scenario() == 14 ? 1 : 0)); + ASSERT(dataCount == burst() + (scenario() == 4 || scenario() == 18 ? 8 : scenario() == 5 || scenario() == 6 || scenario() == 12 || scenario() == 14 || (compressed() && scenario() == 16) ? 1 : compressed() && scenario() == 17 ? 4 : 0)); else { ASSERT(recipient->checkedBitmap && recipient->retired()); ASSERT((recipient->delivered == std::vector{1, 2})); ASSERT(dataCount > 3); ASSERT(expired > 0); + if (compressed()) + ASSERT(expiredDuringExchange); } ASSERT(originatorBlockAckAgreementHandler->getAgreements().empty()); ASSERT(originatorBlockAckAgreementHandler->getProcedures().empty()); @@ -367,6 +488,8 @@ class SchedulingHcf : public Hcf { ASSERT(!frameSequenceHandler->isSequenceRunning()); if (scenario() == 5 || scenario() == 6) ASSERT(retries == 1 && bars == 2); + if (compressed() && scenario() >= 15 && scenario() <= 17) + ASSERT(retries == (scenario() == 15 ? 0 : scenario() == 16 ? 1 : 4) && bars == (scenario() == 15 ? 1 : 2)); if (scenario() == 8 || scenario() == 10 || scenario() == 13) ASSERT(firstBarWasStandalone); if (scenario() == 12 || scenario() == 14) @@ -416,22 +539,23 @@ record-vector-results = false record-scalar-results = false **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${variant} == "basic" ? "a" : "n(mixed-2.4Ghz)" **.wlan[*].bitrate = 6Mbps +**.wlan[*].radio.channelNumber = 0 *.host.wlan[*].address = "02:00:00:00:00:01" *.recipient.wlan[*].address = "02:00:00:00:00:02" **.wlan[*].mac.qosStation = true *.recipient.wlan[*].mac.hcf.typename = "SchedulingRecipient" *.host.wlan[*].mac.hcf.typename = "SchedulingHcf" -**.wlan[*].mac.hcf.scenario = ${scenario=0..14} +**.wlan[*].mac.hcf.scenario = ${scenario=0..18} **.wlan[*].mac.hcf.multiple = ${multiple=false,true} **.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${variant="basic","compressed"} **.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms **.wlan[*].mac.hcf.originatorAckPolicy.blockAckTimeout = 1ms **.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 2346B **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" -**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = ${scenario} == 9 ? 128B : 2346B +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = ${scenario} == 9 && ${variant} == "basic" ? 128B : 2346B **.wlan[*].mac.hcf.edca.edcaf[*].msduLifetime = ${scenario} == 9 ? 10ms : 512ms **.wlan[*].mac.hcf.edca.edcaf[*].cwMin = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 **.wlan[*].mac.hcf.edca.edcaf[2].cwMax = ${scenario} == 14 ? 3 : ${scenario} == 12 ? 0 : -1 diff --git a/tests/module/Ieee80211BlockAckVariant_1.test b/tests/module/Ieee80211BlockAckVariant_1.test index 61da5b30adc..195c298c73a 100644 --- a/tests/module/Ieee80211BlockAckVariant_1.test +++ b/tests/module/Ieee80211BlockAckVariant_1.test @@ -1,8 +1,8 @@ %description: Check profile validation through production HCF initialization. -Retain the default and explicit Basic profile. +Accept the default, explicit Basic, and explicit Compressed profiles. Reject unknown values with support enabled or disabled. -Reject Compressed until its runtime exchange path exists. +Reject delayed policy for Compressed before any ADDBA transmission. %testprog: python3 check-profile.py @@ -18,20 +18,26 @@ import subprocess root = Path.cwd().parents[3] cases = [ - (None, True, None), - ("basic", True, None), - ("invalid", True, 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), - ("invalid", False, 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), - ("compressed", True, 'Compressed Block Ack exchanges are not implemented'), - ("compressed", False, 'Compressed Block Ack exchanges are not implemented'), + (None, True, (), None), + ("basic", True, (), None), + ("invalid", True, (), 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), + ("invalid", False, (), 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), + ("compressed", True, (), None), + ("compressed", False, (), None), ] -for variant, supported, diagnostic in cases: +for policies in [("originator",), ("recipient",), ("originator", "recipient")]: + cases.append(("compressed", True, policies, + "Compressed Block Ack does not support delayedAckPolicySupported")) + cases.append(("basic", True, policies, None)) +for variant, supported, policies, diagnostic in cases: command = [str(root / "bin/inet"), "--debug", "-u", "Cmdenv", "-f", str(Path.cwd() / "profile.ini"), "-c", "General", "-r", "0", "--sim-time-limit=0s", "--record-vector-results=false", "--record-scalar-results=false", f"--result-dir={Path.cwd() / 'results'}", f"--**.wlan[*].mac.hcf.isBlockAckSupported={'true' if supported else 'false'}"] if variant is not None: command.append(f'--**.wlan[*].mac.hcf.blockAckVariant="{variant}"') + for policy in policies: + command.append(f'--**.wlan[*].mac.hcf.{policy}BlockAckAgreementPolicy.delayedAckPolicySupported=true') result = subprocess.run(command, cwd=root / "showcases/wireless/blockack", text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print(result.stdout) diff --git a/tests/module/Ieee80211CompressedBlockAckAdmission_1.test b/tests/module/Ieee80211CompressedBlockAckAdmission_1.test new file mode 100644 index 00000000000..5ef5fb72b25 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckAdmission_1.test @@ -0,0 +1,665 @@ +%description: +Check reversible DATA preparation and Compressed TXOP admission through HCF, EDCA, frame sequences, and Tx. +The fixture supplies accepted agreements and peer capabilities. It injects timed CTS, ACK, and Block Ack responses at HCF. +Service cases check queue ownership, deadlines, sequence assignment, and allocation before refusal and after commitment. +Runtime cases check ordinary fallback, one mode decision, RTS recovery, expiry, capability loss, and mode-set changes. +Ordinary commitment preserves elapsed queueing time and one queue pull; retransmission retains that measurement without another pull. +This fixture does not establish association behavior. The infrastructure exchange fixture supplies that evidence. + +%extraargs: -r 0..23 + +%file: Admission.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class AdmissionService : public OriginatorQosMacDataService { + public: + int preparations = 0; + int commits = 0; + int64_t allocations() const { return nextDataAllocationIndex; } + std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool aggregate) override { + preparations++; + return OriginatorQosMacDataService::prepareFrame(queue, aggregate); + } + Packet *commitPreparedFrame(PreparedQosData& preparation) override { + auto preview = preparation.packet.get(); + auto result = OriginatorQosMacDataService::commitPreparedFrame(preparation); + if (result) { + ASSERT(result == preview); + commits++; + } + return result; + } +}; +Define_Module(AdmissionService); + +class AdmissionRecovery : public QosRecoveryProcedure { + public: + int controls = 0; + int dataFailures = 0; + int numberedRtsFailures = 0; + std::vector qsrcHistory; + int qsrc() const { return stationShortRetryCounter; } + size_t numberedCounters() const { return shortRetryCounter.size() + longRetryCounter.size(); } + void controlFrameTransmissionFailed() override { + controls++; + QosRecoveryProcedure::controlFrameTransmissionFailed(); + qsrcHistory.push_back(stationShortRetryCounter); + } + void dataFrameTransmissionFailed(Packet *packet, const Ptr& header) override { + dataFailures++; + QosRecoveryProcedure::dataFrameTransmissionFailed(packet, header); + } + void rtsFrameTransmissionFailed(const Ptr& header) override { + numberedRtsFailures++; + QosRecoveryProcedure::rtsFrameTransmissionFailed(header); + } +}; +Define_Module(AdmissionRecovery); + +class AdmissionRateControl : public ModeSetListener, public IRateControl { + public: + int queries = 0; + int failedDataFeedback = 0; + const physicallayer::IIeee80211Mode *getRate(const MacAddress&) override { + queries++; + int scenario = getParentModule()->par("scenario"); + int rate = scenario == 22 ? 6 : scenario == 6 || scenario == 19 || scenario == 20 ? (queries % 2 ? 6 : 24) : + scenario == 17 ? (queries <= 2 ? 24 : 6) : queries % 2 ? 24 : 6; + return modeSet->getMode(Mbps(rate)); + } + void frameTransmitted(Packet *packet, int, bool successful, bool) override { + if (!successful && dynamicPtrCast(packet->peekAtFront())) + failedDataFeedback++; + } + void frameReceived(Packet *) override {} +}; +Define_Module(AdmissionRateControl); + +class AdmissionAgreements : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTid(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + request->setAMsduSupported(true); + createAgreement(request); + auto agreement = getAgreement(peer, 5); + agreement->setIsAddbaResponseReceived(true); + agreement->setResponseAMsduSupported(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + finishProcedure(peer, 5); + } +}; + +class AdmissionHcf : public Hcf { + const MacAddress peer{"02:00:00:00:00:02"}; + Ptr pendingRts; + Ptr pendingAck; + bool pendingBasic = false; + Tid pendingTid = -1; + SequenceNumberCyclic pendingStart; + const physicallayer::IIeee80211Mode *responseMode = nullptr; + simtime_t firstDeadline; + bool responsePending = false; + b firstDataLength; + int failuresWanted() const { return scenario() == 7 ? 1 : scenario() == 8 ? 2 : scenario() == 9 ? 1000 : 0; } + public: + std::vector sourceIds; + std::vector sourcePackets; + std::vector sourceOwners; + std::vector deadlines; + int data = 0; + int scopedData = 0; + int rts = 0; + int aggregates = 0; + int refusedSequences = 0; + int mutation = 0; + int queueRemovals = 0; + int aggregationEvents = 0; + int queuePulls = 0; + int ordinaryMeasurements = 0; + simtime_t ordinaryQueueTime; + int internalCollisions = 0; + std::vector queriesAtData; + int scenario() const { return par("scenario"); } + AdmissionService *service() const { return check_and_cast(getSubmodule("originatorMacDataService")); } + AdmissionRecovery *recovery() const { return check_and_cast(edca->getEdcaf(AC_VI)->getRecoveryProcedure()); } + AdmissionRateControl *control() const { return check_and_cast(getSubmodule("rateControl")); } + queueing::IPacketQueue *queue() const { return edca->getEdcaf(AC_VI)->getPendingQueue(); } + EdcaTransmitLifetimeHandler *lifetime() const { return edca->getLifetimeHandler(); } + InProgressFrames *frames() const { return edca->getEdcaf(AC_VI)->getInProgressFrames(); } + void setup() { + Enter_Method("setup"); + rx->setContentionBlocked(true); + Ieee80211RateSetState rates; + rates.supported.known = rates.basic.known = rates.operational.known = true; + rates.supported.legacyRates = rates.basic.legacyRates = rates.operational.legacyRates = {Mbps(6), Mbps(12), Mbps(24)}; + mib->setLocalRateSet(rates); + mib->installBssAndPeerRateSets(rates, peer, rates); + if (scenario() != 16) { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + delete originatorBlockAckAgreementHandler; + auto agreements = new AdmissionAgreements(); + originatorBlockAckAgreementHandler = agreements; + agreements->accept(peer); + } + int firstLength = scenario() == 13 ? 1400 : scenario() == 17 ? 2000 : 700; + if (scenario() == 21) + enqueue(100, 5, false, true); + enqueue(firstLength); + enqueue(scenario() == 17 ? 100 : 700); + if (scenario() == 17) + enqueue(700); + if (scenario() == 18) + enqueue(100, 6, false); + firstDeadline = deadlines.front(); + check_and_cast(queue())->subscribe(packetRemovedSignal, this); + check_and_cast(queue())->subscribe(packetPulledSignal, this); + service()->subscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + } + void enqueue(int bytes, Tid tid = 5, bool record = true, bool group = false) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + auto receiver = group ? MacAddress("01:00:5e:00:00:01") : peer; + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(receiver); + auto packet = new Packet("admission-data", header); + packet->insertAtBack(makeShared(B(bytes))); + packet->insertAtBack(makeShared()); + auto time = packet->addRegionTag(header->getChunkLength(), B(bytes)); + time->setPacketTotalTimesArraySize(1); + time->setPacketTotalTimes(0, SIMTIME_ZERO); + packet->setArrivalTime(simTime()); + processUpperFrame(packet, header); + if (record) { + sourcePackets.push_back(packet); + sourceIds.push_back(packet->getId()); + deadlines.push_back(lifetime()->getDeadline(packet)); + sourceOwners.push_back(packet->getOwner()); + } + } + void unchanged(int expectedAllocations = 0) const { + ASSERT(queue()->getNumPackets() == int(sourceIds.size())); + ASSERT(frames()->getLength() == 0 && service()->allocations() == expectedAllocations); + ASSERT(service()->getNextSequenceNumber(peer, 5) == SequenceNumberCyclic(0)); + for (int i = 0; i < int(sourceIds.size()); i++) { + auto packet = queue()->getPacket(i); + ASSERT(packet == sourcePackets[i] && packet->getId() == sourceIds[i]); + ASSERT(packet->getOwner() == sourceOwners[i]); + ASSERT(packet->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime()->findFrame(sourceIds[i]) == packet && lifetime()->getDeadline(packet) == deadlines[i]); + } + } + void run() { Enter_Method("run"); rx->setContentionBlocked(false); } + void stop() { Enter_Method("stop"); rx->setContentionBlocked(true); } + void serviceCheck() { + Enter_Method("serviceCheck"); + auto preparation = service()->prepareFrame(queue(), scenario() != 1); + ASSERT(preparation && preparation->packet && preparation->sourceIds.size() == (scenario() == 1 ? 1 : 2)); + ASSERT(preparation->aggregate == (scenario() != 1)); + unchanged(); + ASSERT(queueRemovals == 0 && aggregationEvents == 0); + auto preview = preparation->packet.get(); + ASSERT(lifetime()->findFrame(preview->getId()) == nullptr); + ASSERT(service()->isPreparationValid(*preparation, simTime())); + if (scenario() == 0) { + preparation.reset(); + unchanged(); + ASSERT(queueRemovals == 0 && aggregationEvents == 0); + } + else if (scenario() == 1 || scenario() == 2) { + auto committed = service()->commitPreparedFrame(*preparation); + ASSERT(committed == preview && !preparation->packet); + ASSERT(service()->allocations() == 1 && service()->getNextSequenceNumber(peer, 5) == SequenceNumberCyclic(1)); + ASSERT(committed->getTag()->getAllocationIndex() == 0); + ASSERT(lifetime()->findFrame(sourceIds.front()) == nullptr); + ASSERT(lifetime()->getDeadline(committed) == (scenario() == 1 ? firstDeadline : simTime() + SimTime(100, SIMTIME_MS))); + ASSERT(queue()->getNumPackets() == (scenario() == 1 ? 1 : 0)); + ASSERT(queueRemovals == (scenario() == 1 ? 1 : 2)); + ASSERT(aggregationEvents == (scenario() == 1 ? 0 : 1)); + if (scenario() == 1) { + ASSERT(queue()->getPacket(0) == sourcePackets[1]); + ASSERT(lifetime()->getDeadline(sourcePackets[1]) == deadlines[1]); + } + lifetime()->forgetFrame(committed->getId()); + take(committed); + delete committed; + } + else if (scenario() == 3) { + auto removed = queue()->dequeuePacket(); + lifetime()->forgetFrame(removed->getId()); + take(removed); + delete removed; + ASSERT(!service()->isPreparationValid(*preparation, simTime())); + ASSERT(service()->commitPreparedFrame(*preparation) == nullptr); + ASSERT(service()->allocations() == 0 && queue()->getNumPackets() == 1); + } + else if (scenario() == 4) { + ASSERT(!service()->isPreparationValid(*preparation, firstDeadline)); + unchanged(); + } + else { + service()->setEligibility([](const Packet *) { return true; }, [](Packet *) { return false; }); + ASSERT(!service()->isPreparationValid(*preparation, simTime())); + ASSERT(service()->commitPreparedFrame(*preparation) == nullptr); + unchanged(); + } + } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LOCAL) { + // Replace fixed submodules before their parent caches pointers. + getSubmodule("originatorMacDataService")->deleteModule(); + auto service = cModuleType::get("AdmissionService")->create("originatorMacDataService", this); + service->finalizeParameters(); + service->buildInside(); + auto access = getSubmodule("edca"); + for (int i = 0; i < 4; i++) { + auto edcaf = access->getSubmodule("edcaf", i); + edcaf->getSubmodule("recoveryProcedure")->deleteModule(); + auto recovery = cModuleType::get("AdmissionRecovery")->create("recoveryProcedure", edcaf); + recovery->finalizeParameters(); + recovery->buildInside(); + } + } + Hcf::initialize(stage); + } + void channelGranted(IChannelAccess *access) override { + auto collided = edca->getInternallyCollidedEdcafs(); + Hcf::channelGranted(access); + if (scenario() == 18 && !collided.empty()) { + internalCollisions += collided.size(); + ASSERT(frames()->getLength() == 1 && queue()->getNumPackets() == 1); + auto committed = frames()->getFrames(0); + auto header = committed->peekAtFront(); + ASSERT(!header->getAMsduPresent() && !header->getRetry()); + ASSERT(queue()->getPacket(0) == sourcePackets[1]); + ASSERT(lifetime()->getDeadline(sourcePackets[1]) == deadlines[1]); + ASSERT(recovery()->dataFailures == 1 && recovery()->getRetryCount(committed, header) == 1); + } + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == packetRemovedSignal || signal == packetPulledSignal) { + queueRemovals++; + if (signal == packetPulledSignal) + queuePulls++; + } + else if (signal == IOriginatorMacDataService::packetAggregatedSignal) + aggregationEvents++; + else + Hcf::receiveSignal(source, signal, value, details); + } + bool isReceptionInProgress() override { return responsePending || Hcf::isReceptionInProgress(); } + void frameSequenceFinished() override { + if (data == 0 && rts > 0 && scenario() != 9 && mutation == 0) { + if (scenario() != 18) + unchanged(); + ASSERT(recovery()->numberedCounters() == 0); + } + if (scenario() == 13 && data == 1) + refusedSequences++; + Hcf::frameSequenceFinished(); + if ((scenario() >= 22 ? data >= 2 : scenario() == 18 ? scopedData > 0 : data > 0) || mutation > 0) + rx->setContentionBlocked(true); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + auto context = frameSequenceHandler->getContext(); + if (auto protectedRts = dynamicPtrCast(header)) { + auto state = context->getQoSContext()->preparedData.get(); + if (state) { + if (state->preparation) { + ASSERT(state->frame == state->preparation->packet.get()); + ASSERT(!frames()->containsFrame(state->frame)); + if (scenario() != 18) + unchanged(); + } + else + ASSERT(frames()->containsFrame(state->frame)); + } + else + ASSERT(scenario() == 15 || scenario() == 16 || scenario() == 18); + pendingRts = protectedRts; + } + bool accepted = Hcf::transmitFrame(packet, ifs); + if (!accepted) + return false; + header = packet->peekAtFront(); + auto mode = packet->getTag()->getMode(); + auto end = simTime() + ifs + mode->getDuration(packet->getDataLength()); + if (dynamicPtrCast(header)) { + pendingRts = staticPtrCast(header); + rts++; + if (scenario() == 19 || scenario() == 20) { + auto state = context->getQoSContext()->preparedData.get(); + ASSERT(state && !state->frame->peekAtFront()->getAMsduPresent()); + auto required = modeSet->getSifsTime() + state->ctsMode->getDuration(LENGTH_CTS) + + modeSet->getSifsTime() + state->dataMode->getDuration(state->frame->getDataLength()); + if (state->ackMode) + required += modeSet->getSifsTime() + state->ackMode->getDuration(LENGTH_ACK); + ASSERT(pendingRts->getDurationField() >= required); + } + if (rts == 1 && (scenario() == 10 || scenario() == 11 || scenario() == 12)) + scheduleAt(end + SimTime(1, SIMTIME_PS), new cMessage("mutation")); + if (rts > failuresWanted()) { + responsePending = true; + responseMode = rateSelection->computeResponseCtsFrameMode(packet, pendingRts, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_CTS), new cMessage("cts")); + } + } + else if (auto ordinary = dynamicPtrCast(header)) { + data++; + scopedData += ordinary->getTid() == 5 && !ordinary->getReceiverAddress().isMulticast(); + aggregates += ordinary->getAMsduPresent(); + queriesAtData.push_back(control()->queries); + if (scenario() != 15 && scenario() != 16 && ordinary->getTid() == 5 && !ordinary->getReceiverAddress().isMulticast()) { + auto state = context->getQoSContext()->preparedData.get(); + ASSERT(state && frames()->containsFrame(packet) && state->dataMode == mode); + if (!ordinary->getAMsduPresent() && (scenario() == 6 || scenario() == 19 || scenario() == 20 || scenario() == 22)) { + auto offset = ordinary->getChunkLength(); + auto time = packet->findRegionTag(offset, packet->getDataLength() - offset - B(4)); + if (!ordinary->getRetry()) { + ASSERT(time && time->getPacketTotalTimes(0) == simTime() - SimTime(1, SIMTIME_MS)); + ASSERT(queuePulls >= 1); + ordinaryQueueTime = time->getPacketTotalTimes(0); + ordinaryMeasurements++; + } + else if (scenario() == 22) + ASSERT(time && time->getPacketTotalTimes(0) == ordinaryQueueTime && queuePulls == 1); + } + } + if (scenario() == 20) + ASSERT(ordinary->getAckPolicy() == NORMAL_ACK && ordinary->getDurationField() >= modeSet->getSifsTime() + + rateSelection->computeResponseAckFrameMode(packet, ordinary, ResponseQueryRole::PREDICT)->getDuration(LENGTH_ACK)); + if (scenario() >= 22 && data == 2) { + ASSERT(ordinary->getRetry()); + ASSERT(packet->getDataLength() == firstDataLength); + ASSERT(mode->getDuration(packet->getDataLength()) > SimTime(1, SIMTIME_MS)); + ASSERT(ordinary->getAMsduPresent() == (scenario() == 23)); + } + if (scenario() >= 22 && data == 1) { + firstDataLength = packet->getDataLength(); + ASSERT(!ordinary->getRetry()); + return true; // No ACK forces a retry of the same committed MPDU. + } + if (ordinary->getAckPolicy() == NORMAL_ACK) { + responsePending = true; + pendingAck = ordinary; + responseMode = rateSelection->computeResponseAckFrameMode(packet, ordinary, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_ACK), new cMessage("ack")); + } + } + else if (auto management = dynamicPtrCast(header)) { + responsePending = true; + pendingAck = management; + responseMode = rateSelection->computeResponseAckFrameMode(packet, management, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_ACK), new cMessage("ack")); + } + else if (auto bar = dynamicPtrCast(header)) { + responsePending = true; + pendingBasic = dynamicPtrCast(bar) != nullptr; + pendingTid = pendingBasic ? staticPtrCast(bar)->getTidInfo() : + staticPtrCast(bar)->getTidInfo(); + pendingStart = pendingBasic ? staticPtrCast(bar)->getStartingSequenceNumber() : + staticPtrCast(bar)->getStartingSequenceNumber(); + responseMode = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(pendingBasic ? LENGTH_BASIC_BLOCKACK : LENGTH_COMPRESSED_BLOCKACK), new cMessage("ba")); + } + return true; + } + void receive(Packet *packet, const Ptr& header) { + packet->addTag()->setMode(responseMode); + if (!frameSequenceHandler->isSequenceRunning()) { + delete packet; + return; + } + processLowerFrame(packet, header); + } + void handleMessage(cMessage *message) override { + if (message->isName("mutation")) { + delete message; + mutation++; + int queries = control()->queries; + if (scenario() == 10) + mib->removePeerHtCapabilities(peer); + else if (scenario() == 11) { + auto rates = mib->getBssRateSet(); + rates.basic.legacyRates.erase(Mbps(12)); + mib->setBssRateSet(rates); + } + else { + auto radio = check_and_cast(getModuleByPath("^.^.radio")); + radio->setMode(modeSet->getMode(Mbps(24))); + auto replacement = physicallayer::Ieee80211ModeSet::getModeSet("g(mixed)"); + radio->setModeSet(replacement); + getContainingNicModule(this)->emit(modesetChangedSignal, replacement); + } + ASSERT(control()->queries == queries); + if (scenario() != 11) + rx->setContentionBlocked(true); + return; + } + if (message->isName("cts")) { + delete message; + responsePending = false; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + header->setDurationField(std::max(SIMTIME_ZERO, pendingRts->getDurationField() - modeSet->getSifsTime() - responseMode->getDuration(LENGTH_CTS))); + receive(new Packet("synthetic-cts", header), header); + return; + } + if (message->isName("ack")) { + delete message; + responsePending = false; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + receive(new Packet("synthetic-ack", header), header); + return; + } + if (message->isName("ba")) { + delete message; + responsePending = false; + Ptr header; + if (pendingBasic) { + auto basic = makeShared(); + basic->setTidInfo(pendingTid); + basic->setStartingSequenceNumber(pendingStart); + for (int i = 0; i < 64; i++) basic->setBlockAckBitmap(i, BitVector("1000000000000000")); + header = basic; + } + else { + auto compressed = makeShared(); + compressed->setTidInfo(pendingTid); + compressed->setStartingSequenceNumber(pendingStart); + compressed->setBlockAckBitmap(BitVector(std::string(64, '1').c_str())); + header = compressed; + } + header->setReceiverAddress(mac->getAddress()); + header->setTransmitterAddress(peer); + receive(new Packet("synthetic-ba", header), header); + return; + } + Hcf::handleMessage(message); + } +}; +Define_Module(AdmissionHcf); + +class AdmissionTest : public cSimpleModule { + public: + AdmissionTest() : cSimpleModule(131072) {} + protected: + void activity() override { + wait(SimTime(1, SIMTIME_MS)); + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + hcf->setup(); + int scenario = par("scenario"); + if (scenario < 6) + hcf->serviceCheck(); + else { + hcf->run(); + wait(SimTime(20, SIMTIME_MS)); + hcf->stop(); + if (scenario == 6 || scenario == 19 || scenario == 20 || scenario == 22) + ASSERT(hcf->ordinaryMeasurements == 1 && hcf->queuePulls == 1); + auto recovery = hcf->recovery(); + ASSERT(recovery->dataFailures == (scenario == 18 || scenario >= 22 ? 1 : 0)); + ASSERT(hcf->control()->failedDataFeedback == (scenario >= 22 ? 1 : 0)); + if (scenario == 9) { + ASSERT(hcf->rts > 0 && hcf->data == 0 && hcf->queue()->isEmpty()); + ASSERT(hcf->service()->allocations() == 0); + int queries = hcf->control()->queries; + wait(SimTime(1, SIMTIME_MS)); + ASSERT(hcf->control()->queries == queries); + } + else if (scenario == 10 || scenario == 12) { + ASSERT(hcf->data == 0 && hcf->mutation == 1); + hcf->unchanged(); + ASSERT(hcf->control()->queries == 1 && recovery->numberedCounters() == 0); + } + else { + ASSERT(hcf->data > 0); + if (scenario == 6 || scenario == 19 || scenario == 20) { + ASSERT(hcf->data == 1 && hcf->aggregates == 0 && hcf->queriesAtData.front() == 1); + ASSERT(hcf->queue()->getNumPackets() == 1 && hcf->service()->allocations() == 1); + ASSERT(hcf->lifetime()->getDeadline(hcf->queue()->getPacket(0)) == hcf->deadlines[1]); + } + if (scenario == 7 || scenario == 8) { + ASSERT(recovery->controls == (scenario == 7 ? 1 : 2)); + ASSERT(recovery->numberedRtsFailures == 0); + ASSERT(hcf->queriesAtData.front() == (scenario == 7 ? 2 : 3)); + ASSERT(recovery->qsrcHistory[0] == 1); + if (scenario == 8) ASSERT(recovery->qsrcHistory[1] == 0); + } + else + ASSERT(recovery->controls == 0); + if (scenario == 11) + ASSERT(hcf->queriesAtData.front() == 1 && hcf->mutation == 1); + if (scenario == 13) { + ASSERT(hcf->data == 1 && hcf->refusedSequences == 1); + ASSERT(hcf->queue()->getNumPackets() == 1 && hcf->service()->allocations() == 1); + ASSERT(hcf->queue()->getPacket(0) == hcf->sourcePackets[1]); + ASSERT(hcf->lifetime()->getDeadline(hcf->queue()->getPacket(0)) == hcf->deadlines[1]); + } + if (scenario == 14) + ASSERT(hcf->data == 1 && hcf->aggregates == 1 && hcf->queriesAtData.front() == 1); + if (scenario == 15 || scenario == 16) + ASSERT(hcf->service()->preparations == 0); + if (scenario == 17) { + ASSERT(hcf->data == 2 && hcf->aggregates == 0); + ASSERT(hcf->queriesAtData[0] == 1 && hcf->queriesAtData[1] == 2); + ASSERT(hcf->queue()->getNumPackets() == 1 && hcf->service()->allocations() == 2); + } + if (scenario == 18) { + ASSERT(hcf->internalCollisions == 1 && hcf->scopedData == 1 && hcf->aggregates == 0); + ASSERT(hcf->queue()->getNumPackets() == 1); + ASSERT(hcf->lifetime()->getDeadline(hcf->queue()->getPacket(0)) == hcf->deadlines[1]); + } + if (scenario == 21) { + ASSERT(hcf->data == 1 && hcf->scopedData == 0 && hcf->service()->commits == 0); + ASSERT(hcf->service()->preparations > 0); + hcf->unchanged(1); + } + if (scenario >= 22) { + ASSERT(hcf->data == 2 && hcf->aggregates == (scenario == 23 ? 2 : 0)); + ASSERT(hcf->service()->commits == 1 && hcf->service()->allocations() == 1); + ASSERT(hcf->queue()->getNumPackets() == (scenario == 22 ? 1 : 0)); + } + } + } + std::cout << "Compressed admission scenario " << scenario << " passed.\n"; + endSimulation(); + } +}; +Define_Module(AdmissionTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.originator.OriginatorQosMacDataService; +import inet.linklayer.ieee80211.mac.originator.QosRecoveryProcedure; +import inet.linklayer.ieee80211.mac.contract.IRateControl; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module AdmissionService extends OriginatorQosMacDataService { parameters: @class(::inet::ieee80211::AdmissionService); } +simple AdmissionRecovery extends QosRecoveryProcedure { parameters: @class(::inet::ieee80211::AdmissionRecovery); } +simple AdmissionRateControl like IRateControl { parameters: @class(::inet::ieee80211::AdmissionRateControl); } +module AdmissionHcf extends Hcf { parameters: int scenario; @class(::inet::ieee80211::AdmissionHcf); } +simple AdmissionTest { parameters: int scenario; @class(::inet::ieee80211::AdmissionTest); @display("i=block/cogwheel"); } +network AdmissionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: AdhocHost; + test: AdmissionTest; +} + +%inifile: omnetpp.ini +[General] +network = AdmissionNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.test.scenario = ${scenario=0..23} +*.host.wlan[*].opMode = "n(mixed-2.4Ghz)" +*.host.wlan[*].mac.qosStation = true +*.host.wlan[*].mac.hcf.typename = "AdmissionHcf" +*.host.wlan[*].mac.hcf.scenario = ${scenario} +*.host.wlan[*].mac.hcf.isBlockAckSupported = true +*.host.wlan[*].mac.hcf.blockAckVariant = ${scenario} == 15 ? "basic" : "compressed" +*.host.wlan[*].mac.hcf.rateControl.typename = "AdmissionRateControl" +*.host.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = -1bps +*.host.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +*.host.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = ${scenario} == 20 || ${scenario} >= 22 ? 0B : 10000B +*.host.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 +*.host.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.maxAMsduSize = 1600 +*.host.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +*.host.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = 1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].cwMin = ${scenario} == 18 ? 0 : -1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].cwMax = ${scenario} == 18 ? 0 : -1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].aifsn = ${scenario} == 18 ? 2 : -1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].msduLifetime = ${scenario} == 9 ? 250us : 100ms +*.host.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 14 ? 0s : 1ms +*.host.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = ${scenario} == 13 || ${scenario} == 17 || ${scenario} == 19 || ${scenario} == 20 || ${scenario} == 21 ? "multiple" : "single" +*.host.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 500B +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +Compressed admission scenario 23 passed. diff --git a/tests/module/Ieee80211CompressedBlockAckExchange_1.test b/tests/module/Ieee80211CompressedBlockAckExchange_1.test new file mode 100644 index 00000000000..4ccb39e660a --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckExchange_1.test @@ -0,0 +1,428 @@ +%description: +Use real association and management rate publication for two Compressed Block Ack hops. +The STA sends thirty UDP payloads through the AP to the other STA. +Observe accepted ADDBA dialogs, matched Compressed BAR/BA, positive receipt bits, and final agreement retirement. +After setup, remove one BSS basic rate through the MIB API. Require the agreement and HT generation to remain unchanged. +Record actual frame modes, lengths, airtime, IFS, and TXOP bounds for both protection modes at 6 and 24 Mbps. +The medium uses zero propagation time. Compare local receipt completion with the exact TXOP endpoint without a timing allowance. +The fixture enforces DATA and ACK bounds under active Compressed agreements, with the permitted initial MSDU and same-size retry exceptions. +The fixture records overruns outside active agreements separately. StrictAllTiming retains the original aggregate assertion for inherited failures. +Repeat each case with a non-HT link. Require payload delivery without ADDBA or BAR. +Repeat each case with A-MSDU aggregation enabled. Require actual A-MSDUs under accepted agreements at 24 Mbps. +The LowestResponse configuration supplies two extra runs with a 6 Mbps BAR and BA, one per protection mode. + +%extraargs: -r 0..15 + +%file: Exchange.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ExchangeHcf : public Hcf { + struct DataScope { + bool activeCompressed; + AckPolicy policy; + }; + std::map dataScopes; + struct PendingBar { + SequenceNumberCyclic start; + simtime_t bound; + int dataManagementCount; + bool aggregate; + }; + std::map, int> dialogs; + std::map, PendingBar> pendingBars; + simtime_t observedTxopStart = -1; + int dataManagementCount = 0; + bool aggregate = false; + simtime_t controlReceivedAt = -1; + public: + int requests = 0; + int acceptedResponses = 0; + int bars = 0; + int positiveResponses = 0; + int activeData = 0; + int activeAggregates = 0; + int retired = 0; + int initialOverruns = 0; + int generalDataOverruns = 0; + int generalAckOverruns = 0; + int initialAckOverruns = 0; + int initialBlockAckOverruns = 0; + int outsideAgreementDataOverruns = 0; + int outsideAgreementAckOverruns = 0; + int permittedRetryOverruns = 0; + bool refreshedRates = false; + bool clean() const { + std::cout << "Exchange cleanup: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " originator=" << originatorBlockAckAgreementHandler->getAgreements().size() + << " recipient=" << recipientBlockAckAgreementHandler->getAgreements().size() + << " pendingBars=" << pendingBars.size() + << " inactivityTimer=" << inactivityTimer->isScheduled() << " addbaTimer=" << addbaResponseTimer->isScheduled() << "\n"; + for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) { + std::cout << "Exchange procedure: peer=" << entry.first.first << " tid=" << int(entry.first.second) + << " state=" << int(entry.second.state) << " deadline=" << entry.second.responseDeadline << "\n"; + ASSERT(entry.second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::WAITING_FOR_SUCCESSOR); + ASSERT(!entry.second.requestAttempted && entry.second.responseDeadline == SIMTIME_MAX); + } + for (auto category : {AC_BK, AC_BE, AC_VI, AC_VO}) { + std::cout << "Exchange remaining: ac=" << category + << " queued=" << edca->getEdcaf(category)->getPendingQueue()->getNumPackets() + << " committed=" << edca->getEdcaf(category)->getInProgressFrames()->getLength() << "\n"; + ASSERT(edca->getEdcaf(category)->getPendingQueue()->isEmpty()); + ASSERT(edca->getEdcaf(category)->getInProgressFrames()->getLength() == 0); + } + ASSERT(!inactivityTimer->isScheduled() && !addbaResponseTimer->isScheduled()); + return originatorBlockAckAgreementHandler->getAgreements().empty() && + recipientBlockAckAgreementHandler->getAgreements().empty() && pendingBars.empty(); + } + void checkPublishedState(MacAddress peer) { + ASSERT(mib->getLocalRateSet().operational.known); + ASSERT(mib->getBssRateSet().basic.known); + ASSERT(!mib->getBssRateSet().basic.legacyRates.empty()); + auto rates = mib->findPeerRateSet(peer); + ASSERT(rates && rates->operational.known && !rates->operational.legacyRates.empty()); + auto ht = mib->findPeerHtState(peer); + ASSERT(mib->isHtOperationSupported() && ht && ht->valid); + } + protected: + void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, + Packet *receivedPacket, const Ptr& receivedHeader) override { + controlReceivedAt = simTime(); + Hcf::transmitControlResponseFrame(responsePacket, responseHeader, receivedPacket, receivedHeader); + } + void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) override { + auto mode = packet->getTag()->getMode(); + auto airtime = mode->getDuration(packet->getDataLength()); + ASSERT(simTime() - airtime - controlReceivedAt == modeSet->getSifsTime()); + if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == LENGTH_COMPRESSED_BLOCKACK); + std::cout << "Exchange response: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " start=" << simTime() - airtime << " end=" << simTime() << " type=" << header->getType() + << " bytes=" << packet->getByteLength() << " bitrate=" << mode->getDataMode()->getNetBitrate() + << " airtime=" << airtime << " ifs=" << modeSet->getSifsTime() << "\n"; + Hcf::recipientProcessTransmittedControlResponseFrame(packet, header); + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (auto data = dynamicPtrCast(header)) { + auto index = packet->getTag()->getAllocationIndex(); + ASSERT(dataScopes.count(index)); + const auto& scope = dataScopes.at(index); + std::cout << "Exchange scope: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " at=" << simTime() << " allocation=" << index << " activeCompressed=" << scope.activeCompressed + << " ackPolicy=" << scope.policy << " aggregate=" << data->getAMsduPresent() << " retry=" << data->getRetry() << "\n"; + auto owner = edca->getChannelOwner(); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + auto bound = txop->getStart() + txop->getLimit(); + if (simTime() > bound) { + if (par("strictAllTiming").boolValue()) + ASSERT(!data->getAMsduPresent()); + if (scope.activeCompressed) { + ASSERT(dataManagementCount == 1); + ASSERT(!data->getAMsduPresent() || data->getRetry()); + permittedRetryOverruns += data->getRetry(); + } + else + outsideAgreementDataOverruns++; + if (dataManagementCount == 1) + initialOverruns++; + else + generalDataOverruns++; + } + } + Hcf::transmissionComplete(packet, header); + } + void originatorProcessReceivedFrame(Packet *packet, Packet *transmitted) override { + auto data = dynamicPtrCast(transmitted->peekAtFront()); + if (data) { + auto owner = edca->getChannelOwner(); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + if (simTime() > txop->getStart() + txop->getLimit()) { + auto index = transmitted->getTag()->getAllocationIndex(); + const auto& scope = dataScopes.at(index); + if (par("strictAllTiming").boolValue()) + ASSERT(!data->getAMsduPresent()); + if (scope.activeCompressed) { + ASSERT(dataManagementCount == 1); + ASSERT(!data->getAMsduPresent() || data->getRetry()); + } + else + outsideAgreementAckOverruns++; + if (dataManagementCount > 1) + generalAckOverruns++; + else + initialAckOverruns++; + } + } + Hcf::originatorProcessReceivedFrame(packet, transmitted); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + bool activeCompressed = false; + if (auto data = dynamicPtrCast(header)) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + activeCompressed = std::string(par("blockAckVariant").stringValue()) == "compressed" && + agreement && agreement->getIsAddbaResponseReceived(); + } + bool sent = Hcf::transmitFrame(packet, ifs); + if (!sent) + return false; + header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) + dataScopes.insert_or_assign(packet->getTag()->getAllocationIndex(), + DataScope{activeCompressed, data->getAckPolicy()}); + auto owner = edca->getChannelOwner(); + if (owner) { + auto txop = owner->getTxopProcedure(); + if (txop->getStart() != observedTxopStart) { + observedTxopStart = txop->getStart(); + dataManagementCount = 0; + aggregate = false; + } + if (dynamicPtrCast(header)) + dataManagementCount++; + } + if (auto request = dynamicPtrCast(header)) { + checkPublishedState(request->getReceiverAddress()); + dialogs[{request->getReceiverAddress(), request->getTid()}] = request->getDialogToken(); + requests++; + } + if (auto bar = dynamicPtrCast(header)) { + ASSERT(bar->getCompressedBitmap() && !bar->getMultiTid()); + ASSERT(packet->getDataLength() == LENGTH_BLOCKACKREQ); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + pendingBars.insert_or_assign({bar->getReceiverAddress(), bar->getTidInfo()}, + PendingBar{bar->getStartingSequenceNumber(), txop->getStart() + txop->getLimit(), dataManagementCount, aggregate}); + bars++; + } + ASSERT(!dynamicPtrCast(header)); + if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == LENGTH_COMPRESSED_BLOCKACK); + if (auto data = dynamicPtrCast(header)) { + aggregate |= data->getAMsduPresent(); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + if (agreement && agreement->getIsAddbaResponseReceived()) { + ASSERT(data->getFragmentNumber() == 0 && !data->getMoreFragments()); + activeData++; + activeAggregates += data->getAMsduPresent(); + } + } + if (dynamicPtrCast(header)) + retired++; + if (header->getType() == ST_DATA_WITH_QOS || header->getType() == ST_RTS || + header->getType() == ST_CTS || header->getType() == ST_BLOCKACK_REQ || header->getType() == ST_BLOCKACK) { + auto mode = packet->getTag()->getMode(); + std::cout << "Exchange frame: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " at=" << simTime() << " type=" << header->getType() << " bytes=" << packet->getByteLength() + << " bitrate=" << mode->getDataMode()->getNetBitrate() << " ifs=" << ifs + << " airtime=" << mode->getDuration(packet->getDataLength()); + if (owner) { + auto txop = owner->getTxopProcedure(); + std::cout << " txopStart=" << txop->getStart() << " txopLimit=" << txop->getLimit() + << " protection=" << txop->getProtectionMechanism() << " dataManagementCount=" << dataManagementCount; + } + std::cout << "\n"; + } + return true; + } + void processLowerFrame(Packet *packet, const Ptr& header) override { + if (auto response = dynamicPtrCast(header); response && response->getReceiverAddress() == mib->address) { + auto key = std::make_pair(response->getTransmitterAddress(), int(response->getTid())); + ASSERT(dialogs.count(key) && dialogs.at(key) == response->getDialogToken()); + ASSERT(response->getStatusCode() == 0 && response->getBlockAckPolicy()); + acceptedResponses++; + } + if (auto response = dynamicPtrCast(header); response && response->getReceiverAddress() == mib->address) { + auto key = std::make_pair(response->getTransmitterAddress(), int(response->getTidInfo())); + ASSERT(pendingBars.count(key)); + auto pending = pendingBars.at(key); + ASSERT(response->getStartingSequenceNumber() == pending.start); + ASSERT(response->getCompressedBitmap() && !response->getMultiTid()); + ASSERT(response->getBlockAckBitmap().getSize() == 64); + bool positive = false; + for (unsigned int i = 0; i < response->getBlockAckBitmap().getSize(); i++) + positive |= response->getBlockAckBitmap().getBit(i); + ASSERT(positive); + std::cout << "Exchange completion: at=" << simTime() << " bound=" << pending.bound + << " dataManagementCount=" << pending.dataManagementCount << "\n"; + // One initial unaggregated MSDU can overrun. A later exchange must finish within the TXOP. + if (pending.dataManagementCount != 1 || pending.aggregate) + ASSERT(simTime() <= pending.bound); + else if (simTime() > pending.bound) + initialBlockAckOverruns++; + positiveResponses++; + pendingBars.erase(key); + } + ASSERT(!dynamicPtrCast(header)); + Hcf::processLowerFrame(packet, header); + if (auto response = dynamicPtrCast(header); + response && response->getReceiverAddress() == mib->address && !refreshedRates) { + auto peer = response->getTransmitterAddress(); + auto tid = response->getTid(); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, tid); + ASSERT(agreement && agreement->getIsAddbaResponseReceived()); + auto generation = mib->findPeerHtState(peer)->generation; + auto rates = mib->getBssRateSet(); + ASSERT(rates.basic.legacyRates.size() > 1); + rates.basic.legacyRates.erase(std::prev(rates.basic.legacyRates.end())); + // Association supplies the initial rates. This rate-only update exercises the production subscriber. + mib->setBssRateSet(rates); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, tid) == agreement); + ASSERT(mib->findPeerHtState(peer)->generation == generation); + refreshedRates = true; + } + } +}; +Define_Module(ExchangeHcf); + +class ExchangeTest : public cSimpleModule, public cListener { + int received = 0; + int64_t bytes = 0; + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); } + void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + auto packet = check_and_cast(value); + received++; + bytes += packet->getByteLength(); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + getModuleByPath("^.sta2.app[0]")->subscribe(packetReceivedSignal, this); + scheduleAt(SimTime(1200, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + ASSERT(received == 30 && bytes == 21000); + for (const char *path : {"^.sta1.wlan[0].mac.hcf", "^.ap.wlan[0].mac.hcf"}) { + auto hcf = check_and_cast(getModuleByPath(path)); + std::cout << "Exchange summary: node=" << path << " accepted=" << hcf->acceptedResponses + << " bars=" << hcf->bars << " positiveResponses=" << hcf->positiveResponses + << " activeData=" << hcf->activeData << " retired=" << hcf->retired + << " activeAggregates=" << hcf->activeAggregates << " initialOverruns=" << hcf->initialOverruns + << " generalDataOverruns=" << hcf->generalDataOverruns << " generalAckOverruns=" << hcf->generalAckOverruns + << " outsideAgreementDataOverruns=" << hcf->outsideAgreementDataOverruns + << " outsideAgreementAckOverruns=" << hcf->outsideAgreementAckOverruns + << " permittedRetryOverruns=" << hcf->permittedRetryOverruns + << " initialAckOverruns=" << hcf->initialAckOverruns << " initialBlockAckOverruns=" << hcf->initialBlockAckOverruns << "\n"; + if (par("ht").boolValue()) { + ASSERT(hcf->requests > 0 && hcf->acceptedResponses > 0 && hcf->bars > 0); + ASSERT(hcf->positiveResponses > 0 && hcf->positiveResponses <= hcf->bars && hcf->activeData > 0 && hcf->retired > 0); + ASSERT(hcf->refreshedRates); + if (par("aggregate").boolValue() && par("rate").intValue() == 24) + ASSERT(hcf->activeAggregates > 0); + } + else + ASSERT(hcf->requests == 0 && hcf->acceptedResponses == 0 && hcf->bars == 0 && hcf->activeData == 0); + ASSERT(hcf->clean()); + } + ASSERT(check_and_cast(getModuleByPath("^.sta2.wlan[0].mac.hcf"))->clean()); + std::cout << "Compressed infrastructure exchange assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(ExchangeTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ExchangeHcf extends Hcf { parameters: bool strictAllTiming = default(false); @class(::inet::ieee80211::ExchangeHcf); } +simple ExchangeTest { parameters: bool ht; bool aggregate; int rate; @class(::inet::ieee80211::ExchangeTest); } +network ExchangeNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + sta1: WirelessHost; + sta2: WirelessHost; + test: ExchangeTest; +} + +%inifile: omnetpp.ini +[General] +**.hcf.blockAckVariant = "compressed" +network = ExchangeNetwork +ned-path = .;../../../../src +sim-time-limit = 1.3s +seed-set = 0 +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.sta1.numApps = 1 +*.sta1.app[0].typename = "UdpBasicApp" +*.sta1.app[0].destAddresses = "sta2" +*.sta1.app[0].destPort = 1000 +*.sta1.app[0].messageLength = 700B +*.sta1.app[0].startTime = 1s +*.sta1.app[0].stopTime = 1.00295s +*.sta1.app[0].sendInterval = 100us +*.sta2.numApps = 1 +*.sta2.app[0].typename = "UdpSink" +*.sta2.app[0].localPort = 1000 +*.sta*.ipv4.arp.typename = "GlobalArp" +*.test.ht = ${ht=true,false} +*.test.aggregate = ${aggregationPolicy="","BasicMsduAggregationPolicy"} != "" +*.test.rate = ${rate=6,24} +**.wlan[*].opMode = ${ht} ? "n(mixed-2.4Ghz)" : "g(mixed)" +**.wlan[*].bitrate = ${rate}Mbps +**.bandName = "2.4 GHz" +**.wlan[*].radio.antenna.numAntennas = 2 +**.wlan[*].classifier.typename = "QosClassifier" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ExchangeHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.originatorAckPolicy.blockAckReqThreshold = 5 +**.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.originatorMacDataService.msduAggregationPolicy.typename = ${aggregationPolicy} +**.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.originatorMacDataService.msduAggregationPolicy.maxAMsduSize = 1600 +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +**.rtsPolicy.rtsThreshold = 500B +**.txopProcedure.protectionMechanism = ${protection="single","multiple"} +**.txopProcedure.txopLimit = 1ms +**.wlan[*].radio.receiver.energyDetection = -105dBm +**.wlan[*].radio.receiver.sensitivity = -105dBm +**.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sta1.mobility.initialX = 0m +*.ap.mobility.initialX = 5m +*.sta2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +[Config StrictAllTiming] +**.wlan[*].mac.hcf.strictAllTiming = true + +[Config LowestResponse] +constraint = $ht == true && $aggregationPolicy == "" && $rate == 6 +*.test.ht = true +*.test.aggregate = false +*.test.rate = 6 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps + +%contains: stdout +Compressed infrastructure exchange assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckInput_1.test b/tests/module/Ieee80211CompressedBlockAckInput_1.test new file mode 100644 index 00000000000..0775fc9b4a7 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckInput_1.test @@ -0,0 +1,325 @@ +%description: +Check malformed input and profile conflicts through initialized HCF. +Both local profiles receive valid addressed BAR from the opposite profile. +HCF must stop before receipt, reorder, inactivity, recovery, or response updates. +Malformed BAR and unaddressed BAR must not cause a profile conflict. +An active Compressed agreement rejects fragmented DATA before ACK and data-service callbacks. +A valid Compressed BAR without an agreement requests DELBA. A malformed BAR does not. +The fixture supplies an accepted agreement. It does not test ADDBA negotiation or wireless reception. +The separate Compressed exchange fixture tests negotiation and frame exchange. + +%extraargs: -r 0..1 + +%file: CompressedInput.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class InputAgreement : public RecipientBlockAckAgreement { + public: + int refreshes = 0; + using RecipientBlockAckAgreement::RecipientBlockAckAgreement; + void calculateExpirationTime() override { + refreshes++; + RecipientBlockAckAgreement::calculateExpirationTime(); + } +}; +class InputAgreements : public RecipientBlockAckAgreementHandler { + public: + int dataUpdates = 0; + InputAgreement *accept(MacAddress peer, BlockAckVariant variant) { + auto agreement = new InputAgreement(peer, 5, SequenceNumberCyclic(100), 64, SimTime(5, SIMTIME_MS)); + agreement->setVariant(variant); + agreement->addbaResposneSent(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } + void qosFrameReceived(const Ptr& header, IBlockAckAgreementHandlerCallback *callback) override { + dataUpdates++; + RecipientBlockAckAgreementHandler::qosFrameReceived(header, callback); + } +}; +class InputAckSpy : public IRecipientAckProcedure { + public: + IRecipientAckProcedure *delegate; + int calls = 0; + explicit InputAckSpy(IRecipientAckProcedure *delegate) : delegate(delegate) {} + ~InputAckSpy() override { delete delegate; } + void processReceivedFrame(Packet *packet, const Ptr& header, + IRecipientAckPolicy *policy, IProcedureCallback *callback) override { + calls++; + delegate->processReceivedFrame(packet, header, policy, callback); + } + void processTransmittedAck(const Ptr& header) override { + delegate->processTransmittedAck(header); + } +}; +class InputDataSpy : public IRecipientQosMacDataService { + public: + IRecipientQosMacDataService *delegate = nullptr; + int dataCalls = 0; + int controlCalls = 0; + int retireCalls = 0; + std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool reset) override { + retireCalls++; + return delegate->retireAgreement(agreement, reset); + } + void clearPeer(MacAddress peer) override { delegate->clearPeer(peer); } + std::vector dataFrameReceived(Packet *packet, const Ptr& header, + IRecipientBlockAckAgreementHandler *agreements, bool interframe = false) override { + dataCalls++; + return delegate->dataFrameReceived(packet, header, agreements, interframe); + } + std::vector controlFrameReceived(Packet *packet, const Ptr& header, + IRecipientBlockAckAgreementHandler *agreements) override { + controlCalls++; + return delegate->controlFrameReceived(packet, header, agreements); + } + std::vector managementFrameReceived(Packet *packet, const Ptr& header) override { + return delegate->managementFrameReceived(packet, header); + } +}; +class CompressedInputHcf : public Hcf { + protected: + InputDataSpy dataSpy; + InputAckSpy *ackSpy = nullptr; + InputAgreements *agreements = nullptr; + InputAgreement *accepted = nullptr; + BlockAckVariant localVariant = BlockAckVariant::BASIC; + int responses = 0; + int deliveries = 0; + int delbas = 0; + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress absent = MacAddress("02:00:00:00:00:04"); + std::vector receiptSnapshot; + simtime_t expirationSnapshot; + + std::vector receipts() { + std::vector result; + auto record = accepted->getBlockAckRecord(); + for (int sequence = 100; sequence < 164; sequence++) { + if (localVariant == BlockAckVariant::COMPRESSED) + result.push_back(record->hasCompleteReceipt(SequenceNumberCyclic(sequence))); + else + for (int fragment = 0; fragment < 16; fragment++) + result.push_back(record->getAckState(SequenceNumberCyclic(sequence), fragment)); + } + return result; + } + void unchanged() { + ASSERT(agreements->getAgreement(5, peer) == accepted); + ASSERT(!agreements->isTeardownPending(peer, 5)); + ASSERT(accepted->refreshes == 0); + ASSERT(accepted->getExpirationTime() == expirationSnapshot); + ASSERT(accepted->getHighestReceivedSequence() == SequenceNumberCyclic(100)); + ASSERT(receipts() == receiptSnapshot); + if (localVariant == BlockAckVariant::COMPRESSED) + ASSERT(accepted->getBlockAckRecord()->getWindowStart() == SequenceNumberCyclic(100)); + ASSERT(inactivityTimer->isScheduled()); + ASSERT(inactivityTimer->getArrivalTime() == expirationSnapshot); + ASSERT(agreements->dataUpdates == 0); + ASSERT(dataSpy.dataCalls == 0 && dataSpy.controlCalls == 0 && dataSpy.retireCalls == 0); + ASSERT(ackSpy->calls == 0); + ASSERT(responses == 0 && deliveries == 0 && delbas == 0); + } + Ptr makeBar(bool compressed, MacAddress transmitter, bool addressed, + int fragment = 0, bool unsupported = false) { + Ptr bar; + if (unsupported) + bar = makeShared(); + else if (compressed) { + auto concrete = makeShared(); + concrete->setTidInfo(5); + concrete->setFragmentNumber(fragment); + concrete->setStartingSequenceNumber(SequenceNumberCyclic(101)); + bar = concrete; + } + else { + auto concrete = makeShared(); + concrete->setTidInfo(5); + concrete->setFragmentNumber(fragment); + concrete->setStartingSequenceNumber(SequenceNumberCyclic(101)); + bar = concrete; + } + bar->setTransmitterAddress(transmitter); + bar->setReceiverAddress(addressed ? mac->getAddress() : MacAddress("02:00:00:00:00:09")); + return bar; + } + void injectBar(bool compressed, MacAddress transmitter, bool addressed, bool conflict, + int fragment = 0, int extraBytes = 0, bool unsupported = false) { + auto bar = makeBar(compressed, transmitter, addressed, fragment, unsupported); + auto packet = new Packet("bar", bar); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + if (extraBytes) + packet->insertAtBack(makeShared(B(extraBytes))); + packet->insertAtBack(makeShared()); + bool caught = false; + try { + processLowerFrame(packet, bar); + } + catch (const cRuntimeError& error) { + // processLowerFrame takes the Packet before the exception. This wrapper owns its disposal. + take(packet); + delete packet; + std::string message = error.what(); + ASSERT(message.find("Block Ack profile conflict at ") == 0); + ASSERT(message.find("peer=" + peer.str() + " TID=5") != std::string::npos); + ASSERT(message.find(compressed ? "local=basic received=compressed" : "local=compressed received=basic") != std::string::npos); + caught = true; + } + ASSERT(caught == conflict); + } + void injectFragment(int fragment, bool more, AckPolicy policy) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(peer); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(peer); + data->setTid(5); + data->setAckPolicy(policy); + data->setSequenceNumber(SequenceNumberCyclic(101)); + data->setFragmentNumber(fragment); + data->setMoreFragments(more); + auto packet = new Packet("fragment", data); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, data); + } + void sendUp(const std::vector& frames) override { + deliveries += frames.size(); + for (auto frame : frames) { + take(frame); + delete frame; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + responses++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + auto delba = dynamicPtrCast(header); + ASSERT(delba && delba->getReceiverAddress() == absent && delba->getTid() == 5); + ASSERT(!delba->getInitiator() && delba->getReasonCode() == 38); + delbas++; + delete packet; + } + public: + void runChecks() { + Enter_Method("runChecks"); + localVariant = std::string(par("blockAckVariant").stringValue()) == "compressed" ? BlockAckVariant::COMPRESSED : BlockAckVariant::BASIC; + delete recipientBlockAckAgreementHandler; + agreements = new InputAgreements(); + recipientBlockAckAgreementHandler = agreements; + accepted = agreements->accept(peer, localVariant); + ackSpy = new InputAckSpy(recipientAckProcedure); + recipientAckProcedure = ackSpy; + dataSpy.delegate = recipientDataService; + recipientDataService = &dataSpy; + auto seed = makeShared(); + seed->setAckPolicy(BLOCK_ACK); + seed->setSequenceNumber(SequenceNumberCyclic(100)); + accepted->recordReceived(seed); + if (localVariant == BlockAckVariant::COMPRESSED) + accepted->getBlockAckRecord()->compressedFrameReceived(seed); + else + accepted->blockAckPolicyFrameReceived(seed); + receiptSnapshot = receipts(); + expirationSnapshot = accepted->getExpirationTime(); + scheduleInactivityTimer(); + unchanged(); + + bool oppositeCompressed = localVariant == BlockAckVariant::BASIC; + injectBar(oppositeCompressed, peer, true, true); + unchanged(); + injectBar(oppositeCompressed, peer, false, false); + unchanged(); + for (bool compressed : {false, true}) { + injectBar(compressed, peer, true, false, 1); + unchanged(); + injectBar(compressed, peer, true, false, 0, 1); + unchanged(); + injectBar(compressed, peer, true, false, 0, 0, true); + unchanged(); + injectBar(compressed, absent, true, false, 1); + unchanged(); + injectBar(compressed, absent, true, false, 0, 1); + unchanged(); + } + if (localVariant == BlockAckVariant::COMPRESSED) { + for (AckPolicy policy : {NORMAL_ACK, BLOCK_ACK}) { + injectFragment(0, true, policy); + unchanged(); + injectFragment(1, false, policy); + unchanged(); + } + } + injectBar(true, absent, true, false); + ASSERT(delbas == 1 && responses == 0 && deliveries == 0); + ASSERT(agreements->getAgreement(5, absent) == nullptr); + ASSERT(agreements->isTeardownPending(absent, 5)); + ASSERT(agreements->getAgreement(5, peer) == accepted); + ASSERT(receipts() == receiptSnapshot && accepted->refreshes == 0); + std::cout << "Compressed Block Ack input assertions passed.\n"; + } +}; +Define_Module(CompressedInputHcf); +class CompressedInputTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->runChecks(); + endSimulation(); + } +}; +Define_Module(CompressedInputTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module CompressedInputHcf extends Hcf { parameters: @class(::inet::ieee80211::CompressedInputHcf); } +simple CompressedInputTest { parameters: @class(::inet::ieee80211::CompressedInputTest); } +network CompressedInputNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: CompressedInputTest; +} + +%inifile: omnetpp.ini +[General] +network = CompressedInputNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "CompressedInputHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = ${"basic", "compressed"} +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Compressed Block Ack input assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckInterframe_1.test b/tests/module/Ieee80211CompressedBlockAckInterframe_1.test new file mode 100644 index 00000000000..6d9a76be69d --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckInterframe_1.test @@ -0,0 +1,542 @@ +%description: +Check Compressed interframe behavior through physical reception and HCF ACK completion. +The fixture supplies an accepted recipient agreement with four positions. It does not test negotiation. +A raw radio driver transmits HT DATA and receives real HCF responses. +The driver supplies no interframe relation flag to HCF or the data service. +Check SIFS, the exact tolerance boundary, a missing prior receipt, wrap, and unrelated traffic. +Check RIFS with an external HT waveform that omits the six-microsecond signal extension. +The external transmitter changes physical and analog duration together. It retains the actual HT mode and PHY length. +Check a default-extension RIFS control and a gap outside the RIFS tolerance. +Repeat each case with zero and five-microsecond propagation time. +After cache expiry, require the saved physical snapshot to retain the exact interval for a later transmission. + +%extraargs: -r 0..27 + +%file: Interframe.cc +#include +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" +#include "inet/mobility/contract/IMobility.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmitterAnalogModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet::ieee80211 { +using namespace physicallayer; + +class InterframeTransmitter : public Ieee80211Transmitter { + public: + const ITransmission *createTransmission(const IRadio *radio, const Packet *packet, simtime_t start) const override { + if (!packet->isName("first-no-extension")) + return Ieee80211Transmitter::createTransmission(radio, packet, start); + auto selected = computeTransmissionMode(packet); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + ASSERT(selected->getHtMcsIndex() == 0); + ASSERT(selected->getSignalExtensionTime() == SimTime(6, SIMTIME_US)); + auto duration = selected->getDuration(B(phy->getLengthField())) - SimTime(6, SIMTIME_US); + auto preamble = selected->getPreambleMode()->getDuration(); + auto header = selected->getHeaderMode()->getDuration(); + auto data = duration - preamble - header; + auto mobility = radio->getAntenna()->getMobility(); + auto position = mobility->getCurrentPosition(); + auto orientation = mobility->getCurrentAngularPosition(); + auto analog = getAnalogModel()->createAnalogModel(preamble, header, data, centerFrequency, + selected->getDataMode()->getBandwidth(), computeTransmissionPower(packet)); + return new Ieee80211Transmission(radio, packet, start, start + duration, preamble, header, data, + position, position, orientation, orientation, nullptr, nullptr, nullptr, nullptr, + analog, selected, computeTransmissionChannel(packet)); + } +}; +Define_Module(InterframeTransmitter); + +class InterframeReordering : public BlockAckReordering { + public: + ReceiveBuffer *find(MacAddress peer) const { + auto it = receiveBuffers.find({5, peer}); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; +class InterframeRecipient : public RecipientQosMacDataService { + protected: + void initialize() override { + RecipientQosMacDataService::initialize(); + delete blockAckReordering; + blockAckReordering = new InterframeReordering(); + } + public: + ReceiveBuffer *buffer(MacAddress peer) const { + return static_cast(blockAckReordering)->find(peer); + } +}; +Define_Module(InterframeRecipient); + +class InterframeJammer : public cSimpleModule { + public: + void start() { + Enter_Method("start"); + scheduleAfter(SimTime(1, SIMTIME_PS), new cMessage("busy")); + } + protected: + void handleMessage(cMessage *message) override { + delete message; + auto radio = check_and_cast(getParentModule()->getSubmodule("radio")); + radio->setRadioMode(IRadio::RADIO_MODE_TRANSMITTER); + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:09")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:08")); + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet("busy-only", header); + auto fcs = makeShared(); + fcs->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(fcs); + auto catalog = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + const IIeee80211Mode *mode = nullptr; + for (int i = 0; i < catalog->getNumModes(); i++) { + auto candidate = catalog->getMode(i); + if (candidate->getHtMcsIndex() == 8 && !candidate->isHtShortGuardInterval() && + candidate->getDataMode()->getBandwidth() == MHz(20)) + mode = candidate; + } + ASSERT(mode && mode->getDataMode()->getNumberOfSpatialStreams() == 2); + packet->addTag()->setMode(mode); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + send(packet, "out"); + } +}; +Define_Module(InterframeJammer); + +class InterframeAgreements : public RecipientBlockAckAgreementHandler { + public: + RecipientBlockAckAgreement *accept(MacAddress peer, int start) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 4, SIMTIME_ZERO); + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } +}; +class InterframeHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + InterframeRecipient *recipient = nullptr; + InterframeAgreements *agreements = nullptr; + RecipientBlockAckAgreement *accepted = nullptr; + std::vector delivered; + int ackCompletions = 0; + int ctsCompletions = 0; + int baCompletions = 0; + int physicalStarts = 0; + int busyStates = 0; + MacAddress address() const { return mac->getAddress(); } + void prepare(int start) { + Enter_Method("prepare"); + delete recipientBlockAckAgreementHandler; + agreements = new InterframeAgreements(); + recipientBlockAckAgreementHandler = agreements; + accepted = agreements->accept(peer, start); + recipient = check_and_cast(getSimulation()->getSystemModule()->getSubmodule("recipientProbe")); + recipientDataService = recipient; + } + void retireAndAcceptAgain(int start) { + Enter_Method("retireAndAcceptAgain"); + retireAgreement(peer, 5, false, true); + accepted = agreements->accept(peer, start); + // Supply prior receipt state independently of the previous agreement's physical ACK context. + auto seed = makeShared(); + seed->setSequenceNumber(SequenceNumberCyclic(start)); + accepted->recordReceived(seed); + accepted->getBlockAckRecord()->compressedFrameReceived(seed); + } + void check(int start, int scenario, bool report = true) { + Enter_Method("check"); + int first = (start + (scenario == 2 || scenario == 12 ? 1 : 0)) % 4096; + int last = (start + 4) % 4096; + int recordStart = (start + 1) % 4096; + bool related = scenario == 0 || scenario == 3 || scenario == 4 || scenario == 6 || scenario == 7; + auto record = accepted->getBlockAckRecord(); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(recordStart)); + unsigned bitmap = 0; + for (int i = 0; i < 64; i++) + if (record->getCompressedAckState(SequenceNumberCyclic((recordStart + i) % 4096))) + bitmap |= 1u << i; + ASSERT(bitmap == (scenario == 2 || scenario == 12 ? 9u : 8u)); + auto buffer = recipient->buffer(peer); + ASSERT(buffer); + int expectedB = related ? (last + 1) % 4096 : scenario == 2 || scenario == 12 ? (start + 2) % 4096 : recordStart; + ASSERT(buffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(expectedB)); + ASSERT(buffer->getLength() == (related ? 0 : 1)); + std::vector expected = related ? std::vector{first, last} : std::vector{first}; + ASSERT(delivered == expected); + ASSERT(ackCompletions == (scenario >= 7 && scenario <= 9 ? 1 : 2)); + ASSERT(ctsCompletions == (scenario == 5 ? 1 : 0)); + ASSERT(baCompletions == (scenario == 12 ? 1 : 0)); + if (scenario == 13 && report) { + ASSERT(physicalStarts == 2); + ASSERT(busyStates > 0); + } + if (report) + std::cout << "Interframe scenario=" << scenario << " R=" << recordStart << " bitmap=" << bitmap + << " B=" << expectedB << " ACK=" << ackCompletions << " CTS=" << ctsCompletions << "\n"; + } + void checkNoPending() { + Enter_Method("checkNoPending"); + auto watch = dynamic_cast(findObject("numPendingInterframeReceptions", false)); + ASSERT(watch && watch->str() == "0"); + } + protected: + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered.push_back(frame->peekAtFront()->getSequenceNumber().get()); + take(frame); + delete frame; + } + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (header->getType() == ST_ACK) + ackCompletions++; + if (header->getType() == ST_CTS) + ctsCompletions++; + if (header->getType() == ST_BLOCKACK) + baCompletions++; + Hcf::transmissionComplete(packet, header); + if (header->getType() == ST_ACK && ackCompletions == 1 && par("busyProbe").boolValue()) + check_and_cast(getSimulation()->getSystemModule()->getModuleByPath("jammerNode.jammer"))->start(); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (source == getParentModule()->getParentModule()->getSubmodule("radio") && signal == receptionStartedSignal) + physicalStarts++; + Hcf::receiveSignal(source, signal, object, details); + } + void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override { + if (source == getParentModule()->getParentModule()->getSubmodule("radio") && + signal == IRadio::receptionStateChangedSignal && value == IRadio::RECEPTION_STATE_BUSY) + busyStates++; + Hcf::receiveSignal(source, signal, value, details); + } +}; +Define_Module(InterframeHcf); + +class InterframeDriver : public cSimpleModule, public cListener { + protected: + InterframeHcf *receiver = nullptr; + Ieee80211Radio *radio = nullptr; + const IRadioMedium *medium = nullptr; + const IIeee80211Mode *ht = nullptr; + const Ieee80211ModeSet *catalog = nullptr; + int scenario = -1; + int start = 100; + int receivedAcks = 0; + bool finalSent = false; + TransmissionTimingSnapshot saved; + simtime_t initialAckArrival = -1; + bool snapshotVerified = false; + bool completed = false; + int malformedIndex = 0; + + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("start")); + } + void finish() override { ASSERT(completed); } + void scheduleFinal(simtime_t gap) { + scheduleAfter(gap, new cMessage("final")); + } + void sendFrame(const Ptr& header, const char *name, bool payload = false) { + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet(name, header); + if (payload) + packet->insertAtBack(makeShared(B(10))); + auto fcs = makeShared(); + fcs->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(fcs); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + packet->addTag()->setMode(header->getType() == ST_RTS ? catalog->getMode(bps(6000000)) : ht); + send(packet, "out"); + } + void data(int sequence, const char *name, bool addressed = true, AckPolicy policy = NORMAL_ACK) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(addressed ? receiver->address() : MacAddress("02:00:00:00:00:09")); + header->setTransmitterAddress(receiver->peer); + header->setAddress3(receiver->peer); + header->setTid(5); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setAckPolicy(policy); + sendFrame(header, name, true); + } + void sendMalformed() { + Ptr bar; + int kind = malformedIndex % 3; + if (kind == 2) + bar = makeShared(); + else { + auto compressed = makeShared(); + compressed->setStartingSequenceNumber(SequenceNumberCyclic((start + 2) % 4096)); + compressed->setTidInfo(5); + compressed->setFragmentNumber(kind == 0 ? 1 : 0); + bar = compressed; + } + bar->setTransmitterAddress(receiver->peer); + bar->setReceiverAddress(receiver->address()); + sendFrame(bar, "malformed-bar", kind == 1); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + Enter_Method_Silent(); + auto transmission = check_and_cast(object); + auto name = std::string(transmission->getPacket()->getName()); + if (signal == transmissionStartedSignal && name == "snapshot-probe") { + ASSERT(saved.transmissionId >= 0); + ASSERT(medium->getTransmission(saved.transmissionId) == nullptr); + ASSERT(medium->getTransmissionTimingSnapshot(saved.transmissionId).transmissionId == -1); + auto gap = medium->computePrecedingSignalGap(transmission->getId(), saved); + ASSERT(gap.has_value()); + ASSERT(*gap == transmission->getStartTime() - saved.arrivalEndTimes.at(radio->getId())); + snapshotVerified = true; + } + if (signal == transmissionEndedSignal && name == "unrelated") + scheduleFinal(catalog->getSifsTime() + (scenario == 11 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10 + 1) : SIMTIME_ZERO)); + if (signal == transmissionEndedSignal && (name == "first-no-extension" || name == "first-rifs-default")) { + auto extension = transmission->getSignalExtensionDuration(); + ASSERT(extension.has_value()); + ASSERT(*extension == (scenario == 8 ? SimTime(6, SIMTIME_US) : SIMTIME_ZERO)); + scheduleFinal(SimTime(2, SIMTIME_US) + (scenario == 9 ? SimTime(200001, SIMTIME_PS) : SIMTIME_ZERO)); + } + if (signal == transmissionEndedSignal && name == "snapshot-probe") + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage("finish")); + if (signal == transmissionEndedSignal && name == "malformed-bar") { + auto propagation = getSimulation()->getSystemModule()->getSubmodule("radioMedium")->getSubmodule("propagation"); + scheduleAfter(SimTime(propagation->par("propagationTime").doubleValue()) + SimTime(1, SIMTIME_PS), + new cMessage("malformed-check")); + } + } + void handleMessage(cMessage *message) override { + if (!message->isSelfMessage()) { + auto packet = check_and_cast(message); + ASSERT(!packet->hasBitError()); + if (packet->isName("busy-only")) { + delete packet; + return; + } + auto header = packet->peekAtFront(); + if (header->getType() == ST_BLOCKACK) { + ASSERT(scenario == 12 && receivedAcks == 0); + scheduleAfter(catalog->getSifsTime(), new cMessage("first")); + } + else if (header->getType() == ST_CTS) { + ASSERT(scenario == 5 && receivedAcks == 1); + scheduleFinal(catalog->getSifsTime()); + } + else { + ASSERT(header->getType() == ST_ACK); + receivedAcks++; + if (receivedAcks == 1 && (scenario < 7 || scenario >= 10)) { + auto timing = packet->getTag(); + saved = medium->getTransmissionTimingSnapshot(timing->getTransmissionId()); + initialAckArrival = timing->getEndTime(); + ASSERT(initialAckArrival == simTime()); + if (scenario == 10) + receiver->retireAndAcceptAgain(start); + if (scenario == 4 || scenario == 11) + scheduleAfter(catalog->getSifsTime(), new cMessage("unrelated")); + else if (scenario == 5) + scheduleAfter(catalog->getSifsTime(), new cMessage("rts")); + else { + auto offset = scenario == 1 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10 + 1) : + scenario == 6 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10) : SIMTIME_ZERO; + scheduleFinal(catalog->getSifsTime() + offset); + } + } + else { + ASSERT(finalSent); + if (saved.transmissionId < 0) + saved = medium->getTransmissionTimingSnapshot(packet->getTag()->getTransmissionId()); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("snapshot")); + } + } + delete packet; + return; + } + std::string name = message->getName(); + delete message; + if (name == "start") { + receiver = check_and_cast(getSimulation()->getSystemModule()->getModuleByPath("receiver.wlan[0].mac.hcf")); + radio = check_and_cast(getParentModule()->getSubmodule("radio")); + medium = radio->getMedium(); + ASSERT(medium->getTransmission(0) == nullptr); + ASSERT(medium->getTransmissionTimingSnapshot(0).transmissionId == -1); + ASSERT(!medium->computePrecedingSignalGap(0, TransmissionTimingSnapshot()).has_value()); + radio->setRadioMode(IRadio::RADIO_MODE_TRANSCEIVER); + radio->subscribe(transmissionStartedSignal, this); + radio->subscribe(transmissionEndedSignal, this); + scenario = par("scenario"); + start = scenario == 3 ? 4094 : 100; + catalog = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto candidate = catalog->getMode(i); + if (candidate->getHtMcsIndex() == 0 && !candidate->isHtShortGuardInterval() && + candidate->getDataMode()->getBandwidth() == MHz(20)) + ht = candidate; + } + ASSERT(ht); + receiver->prepare(start); + bool rifs = scenario >= 7 && scenario <= 9; + if (scenario == 12) { + auto bar = makeShared(); + bar->setTransmitterAddress(receiver->peer); + bar->setReceiverAddress(receiver->address()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(101)); + sendFrame(bar, "initial-bar"); + } + else + data((start + (scenario == 2 ? 1 : 0)) % 4096, + rifs ? scenario == 8 ? "first-rifs-default" : "first-no-extension" : "first", + true, rifs ? BLOCK_ACK : NORMAL_ACK); + } + else if (name == "first") + data(101, "first"); + else if (name == "final") { + finalSent = true; + data((start + 4) % 4096, "final"); + } + else if (name == "unrelated") + data(200, "unrelated", false); + else if (name == "rts") { + auto header = makeShared(); + header->setTransmitterAddress(receiver->peer); + header->setReceiverAddress(receiver->address()); + sendFrame(header, "rts"); + } + else if (name == "snapshot") { + receiver->check(start, scenario); + data(300, "snapshot-probe", false); + } + else if (name == "finish") { + ASSERT(snapshotVerified); + ASSERT(receivedAcks == (scenario >= 7 && scenario <= 9 ? 1 : 2)); + sendMalformed(); + } + else if (name == "malformed-check") { + receiver->checkNoPending(); + receiver->check(start, scenario, false); + malformedIndex++; + if (malformedIndex < 6) { + sendMalformed(); + return; + } + std::cout << "Compressed Block Ack interframe assertions passed.\n"; + completed = true; + endSimulation(); + } + } +}; +Define_Module(InterframeDriver); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +import inet.mobility.static.StationaryMobility; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Transmitter; +module InterframeTransmitter extends Ieee80211Transmitter { parameters: @class(::inet::ieee80211::InterframeTransmitter); } +module InterframeRecipient extends RecipientQosMacDataService { parameters: @class(::inet::ieee80211::InterframeRecipient); } +module InterframeHcf extends Hcf { parameters: bool busyProbe; @class(::inet::ieee80211::InterframeHcf); } +simple InterframeJammer { + parameters: @class(::inet::ieee80211::InterframeJammer); + gates: output out; +} +simple InterframeDriver { + parameters: int scenario; @class(::inet::ieee80211::InterframeDriver); + gates: input in; output out; +} +module DriverNode { + parameters: @networkNode; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + driver: InterframeDriver; + connections allowunconnected: + driver.out --> radio.upperLayerIn; + radio.upperLayerOut --> driver.in; +} +module JammerNode { + parameters: @networkNode; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + jammer: InterframeJammer; + connections allowunconnected: + jammer.out --> radio.upperLayerIn; +} +network InterframeNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + receiver: WirelessHost; + driverNode: DriverNode; + jammerNode: JammerNode; + recipientProbe: InterframeRecipient; +} + +%inifile: omnetpp.ini +[General] +network = InterframeNetwork +ned-path = .;../../../../src +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.driverNode.driver.scenario = ${scenario=0..13} +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = (${scenario} == 13 ? 100us : 0s) + ${propagation=0s,5us} +*.radioMedium.mediumLimitCache.maxTransmissionDuration = 1ms +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "InterframeHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +*.receiver.wlan[0].mac.hcf.busyProbe = ${scenario} == 13 +*.receiver.wlan[0].radio.bandwidth = 20MHz +*.receiver.wlan[0].radio.antenna.numAntennas = 1 +*.receiver.wlan[0].radio.receiver.sensitivity = -70dBm +*.receiver.wlan[0].radio.receiver.energyDetection = -90dBm +*.jammerNode.radio.opMode = "n(mixed-2.4Ghz)" +*.jammerNode.radio.bandwidth = 20MHz +*.jammerNode.radio.transmitter.bitrate = 6.5Mbps +*.jammerNode.radio.transmitter.power = -40dBm +*.jammerNode.radio.antenna.numAntennas = 2 +*.jammerNode.radio.antenna.mobilityModule = "^.^.mobility" +*.driverNode.radio.opMode = "n(mixed-2.4Ghz)" +*.driverNode.radio.bandwidth = 20MHz +*.driverNode.radio.transmitter.typename = "InterframeTransmitter" +*.driverNode.radio.transmitter.bitrate = 6.5Mbps +*.driverNode.radio.antenna.mobilityModule = "^.^.mobility" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 10m +**.mobility.constraintAreaMaxY = 10m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +*.receiver.mobility.initialX = 0m +*.driverNode.mobility.initialX = 1m +*.jammerNode.mobility.initialX = 1m + +%contains: stdout +Compressed Block Ack interframe assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckOriginator_1.test b/tests/module/Ieee80211CompressedBlockAckOriginator_1.test new file mode 100644 index 00000000000..10bac06324e --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckOriginator_1.test @@ -0,0 +1,175 @@ +%description: +Check Compressed responses through the HCF control callback and real EDCAF state. +The fixture supplies accepted agreements and numbered frames with known ACK states. +The fixture supplies peer HT capabilities; it does not test capability negotiation. +An earlier response acknowledges a sent frame outside the current BAR wait. +Later and disjoint responses release every current BAR wait for retry. +Unsent frames, frames outside the active window, other peers, and other TIDs retain their state. +Cases use ordinary sequence numbers and wrap across 4095. +Scheduling_1 separately checks shifted responses through an active frame sequence. + +%extraargs: -r 0..5 + +%file: Originator.cc +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet::ieee80211 { +class OriginatorAgreements : public OriginatorBlockAckAgreementHandler { + public: + OriginatorBlockAckAgreement *accept(MacAddress peer, int start) { + auto agreement = new OriginatorBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } +}; +class OriginatorHcf : public Hcf { + public: + void check() { + Enter_Method("check"); + rx->setContentionBlocked(true); + const MacAddress peer("02:00:00:00:00:02"); + const MacAddress other("02:00:00:00:00:03"); + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + int start = par("start"); + int offset = par("offset"); + delete originatorBlockAckAgreementHandler; + auto agreements = new OriginatorAgreements(); + originatorBlockAckAgreementHandler = agreements; + auto agreement = agreements->accept(peer, start); + auto edcaf = edca->getEdcaf(AC_VI); + auto frames = edcaf->getInProgressFrames(); + auto ack = edcaf->getAckHandler(); + auto add = [&](MacAddress receiver, Tid tid, int position, bool sent) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setTid(tid); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(start) + position); + auto packet = new Packet("supplied-data", header); + packet->insertAtBack(makeShared()); + frames->adoptPreparedFrame(packet); + if (sent) + ack->processTransmittedDataOrMgmtFrame(header); + return packet; + }; + auto earlier = add(peer, 5, 2, true); + auto earlierHeader = earlier->peekAtFront(); + auto requested = add(peer, 5, 4, true); + auto requestedHeader = requested->peekAtFront(); + auto unsent = add(peer, 5, 5, false); + auto outside = add(peer, 5, 64, true); + auto otherTid = add(peer, 6, 2, true); + auto otherPeer = add(other, 5, 2, true); + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + ack->processTransmittedBlockAckReq(request, agreement); + ASSERT(ack->getQoSDataAckStatus(earlierHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(ack->getQoSDataAckStatus(requestedHeader) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setReceiverAddress(mac->getAddress()); + response->setTidInfo(5); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + offset); + BitVector bits; + for (int bit = 0; bit < 64; bit++) + bits.appendBit((offset == 0 && (bit == 2 || bit == 5)) || (offset == 6 && bit == 58) || offset == 100); + response->setBlockAckBitmap(bits); + auto packet = new Packet("supplied-response", response); + packet->insertAtBack(makeShared()); + originatorProcessReceivedControlFrame(packet, response, nullptr, request, AC_VI); + delete packet; + ASSERT(frames->containsFrame(earlier) == (offset != 0)); + if (offset != 0) + ASSERT(ack->getQoSDataAckStatus(earlierHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(frames->containsFrame(requested)); + ASSERT(ack->isEligibleToTransmit(requestedHeader)); + ASSERT(frames->containsFrame(unsent)); + ASSERT(ack->getQoSDataAckStatus(unsent->peekAtFront()) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + for (auto untouched : {outside, otherTid, otherPeer}) { + ASSERT(frames->containsFrame(untouched)); + ASSERT(ack->getQoSDataAckStatus(untouched->peekAtFront()) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + } + ASSERT(frames->getLength() == (offset == 0 ? 5 : 6)); + std::cout << "Compressed HCF originator assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(OriginatorHcf); +class OriginatorTest : public cSimpleModule { + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("check")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->check(); + } +}; +Define_Module(OriginatorTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module OriginatorHcf extends Hcf { + parameters: + int start; + int offset; + @class(::inet::ieee80211::OriginatorHcf); +} +simple OriginatorTest { parameters: @class(::inet::ieee80211::OriginatorTest); } +network OriginatorNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: OriginatorTest; +} + +%inifile: omnetpp.ini +[General] +network = OriginatorNetwork +ned-path = .;../../../../src +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "OriginatorHcf" +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.start = ${start=100,4094} +**.wlan[*].mac.hcf.offset = ${offset=0,6,100} +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Compressed HCF originator assertions passed. diff --git a/tests/module/Ieee80211HcfMultipleProtection_2.test b/tests/module/Ieee80211HcfMultipleProtection_2.test index 1d278478756..bd2ef75b3d2 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_2.test +++ b/tests/module/Ieee80211HcfMultipleProtection_2.test @@ -2,8 +2,11 @@ Exercise the existing immediate Basic Block Ack exchange through HCF. Check zero-limit RTS estimates with an absent agreement, a Block Ack agreement, AARF query effects, and a deterministic rate decrease. Check response conversion at Tx. +Retain all seven existing scenarios. Add Compressed exchanges with positive and zero TXOP limits. +Repeat negative and fractional Duration/ID conversion with the 32-byte Compressed response. +The Compressed cases supply valid HT peer state and accepted agreements where the exchange requires them. -%extraargs: -r 0..6 +%extraargs: -r 0..9 %file: ProtectionRates.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -16,6 +19,8 @@ AARF query effects, and a deterministic rate decrease. Check response conversion #include "inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class RateProtectionRx : public Rx { @@ -41,14 +46,17 @@ class SeededOriginator : public OriginatorBlockAckAgreementHandler { }; class SeededRecipient : public RecipientBlockAckAgreementHandler { public: - void seed(const MacAddress& peer) { + void seed(const MacAddress& peer, bool compressed = false) { auto request = makeShared(); request->setTransmitterAddress(peer); request->setTid(5); request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setBufferSize(64); request->setBlockAckTimeoutValue(0); - addAgreement(request)->addbaResposneSent(); + auto agreement = addAgreement(request); + if (compressed) + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); } }; @@ -88,6 +96,8 @@ class RateProtectionMac : public Ieee80211Mac { std::vector ends; std::vector durations; std::vector rates; + std::vector lengths; + std::vector compressedFrames; bool suppressed = false; void sendDownFrame(Packet *packet) override { auto header = packet->peekAtFront(); @@ -97,6 +107,9 @@ class RateProtectionMac : public Ieee80211Mac { ends.push_back(simTime() + mode->getDuration(packet->getDataLength())); durations.push_back(header->getDurationField()); rates.push_back(mode->getDataMode()->getNetBitrate()); + lengths.push_back(packet->getDataLength()); + compressedFrames.push_back(bool(dynamicPtrCast(header)) || + bool(dynamicPtrCast(header))); Ieee80211Mac::sendDownFrame(packet); } protected: @@ -126,11 +139,21 @@ class RateProtectionHcf : public Hcf { handler->seed(MacAddress("02:00:00:00:00:02")); originatorBlockAckAgreementHandler = handler; } - void seedRecipient() { + void seedHtPeer(MacAddress peer) { + Enter_Method("seedHtPeer"); + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + auto radio = check_and_cast(getParentModule()->getParentModule()->getSubmodule("radio")); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + ASSERT(mib->findPeerHtState(peer) && mib->findPeerHtState(peer)->valid); + } + void seedRecipient(bool compressed = false) { Enter_Method("seedRecipient"); delete recipientBlockAckAgreementHandler; auto handler = new SeededRecipient(); - handler->seed(MacAddress("02:00:00:00:00:01")); + handler->seed(MacAddress("02:00:00:00:00:01"), compressed); recipientBlockAckAgreementHandler = handler; } void inject(int payload = 66, bool fragment = false) { @@ -148,21 +171,40 @@ class RateProtectionHcf : public Hcf { packet->insertAtBack(makeShared()); processUpperFrame(packet, header); } - void sendSyntheticBlockAck(simtime_t calculatedField) { + void sendSyntheticBlockAck(simtime_t calculatedField, bool compressed = false) { Enter_Method("sendSyntheticBlockAck"); - auto request = makeShared(); + Ptr request = compressed ? Ptr(makeShared()) : + Ptr(makeShared()); + if (compressed) { + auto bar = dynamicPtrCast(request); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + } request->setTransmitterAddress(MacAddress("02:00:00:00:00:07")); request->setReceiverAddress(mac->getAddress()); auto received = new Packet("synthetic-bar", request); auto mode = modeSet->getMode(bps(6000000)); setFrameMode(received, request, mode); - auto updatedRequest = received->removeAtFront(); - updatedRequest->setDurationField(calculatedField + modeSet->getSifsTime() + mode->getDuration(LENGTH_BASIC_BLOCKACK)); + auto updatedRequest = received->removeAtFront(); + updatedRequest->setDurationField(calculatedField + modeSet->getSifsTime() + + mode->getDuration(compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK)); received->insertAtFront(updatedRequest); request = updatedRequest; - auto response = makeShared(); + Ptr response; + if (compressed) { + auto ba = makeShared(); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(0)); + ba->setBlockAckBitmap(BitVector(0u, 64)); + response = ba; + } + else + response = makeShared(); response->setReceiverAddress(request->getTransmitterAddress()); - response->setDurationField(recipientAckPolicy->computeBasicBlockAckDurationField(received, request)); + response->setDurationField(compressed ? recipientAckPolicy->computeCompressedBlockAckDurationField(received, + dynamicPtrCast(request)) : + recipientAckPolicy->computeBasicBlockAckDurationField(received, + dynamicPtrCast(request))); auto packet = new Packet("synthetic-block-ack", response); Hcf::transmitControlResponseFrame(packet, response, received, request); delete received; @@ -199,17 +241,25 @@ class RateProtectionTest : public cSimpleModule { auto receiver = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); auto senderMac = check_and_cast(getModuleByPath("^.sender.wlan[0].mac")); auto receiverMac = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac")); - int scenario = senderMac->par("scenario"); + int configuredScenario = senderMac->par("scenario"); + bool compressed = configuredScenario >= 7; + int scenario = configuredScenario == 7 ? 0 : configuredScenario == 8 ? 1 : configuredScenario == 9 ? 5 : configuredScenario; + if (compressed) { + sender->seedHtPeer(MacAddress("02:00:00:00:00:02")); + receiver->seedHtPeer(MacAddress("02:00:00:00:00:01")); + } if (scenario == 0 || scenario == 1) { sender->seedOriginator(); - receiver->seedRecipient(); + receiver->seedRecipient(compressed); } if (scenario == 2 || scenario == 3) check_and_cast(getModuleByPath("^.sender.wlan[0].mac.hcf.rateControl"))->seedPeer(); if (scenario == 5) { - receiver->sendSyntheticBlockAck(SimTime(-10, SIMTIME_US)); + if (compressed) + receiver->seedHtPeer(MacAddress("02:00:00:00:00:07")); + receiver->sendSyntheticBlockAck(SimTime(-10, SIMTIME_US), compressed); wait(SimTime(300, SIMTIME_US)); - receiver->sendSyntheticBlockAck(SimTime(31.2e-6)); + receiver->sendSyntheticBlockAck(SimTime(31.2e-6), compressed); } else { sender->inject(66, scenario == 6); @@ -223,6 +273,19 @@ class RateProtectionTest : public cSimpleModule { ASSERT(sender->managementAcknowledgments == 1); ASSERT(std::find(senderMac->types.begin(), senderMac->types.end(), ST_BLOCKACK_REQ) != senderMac->types.end()); ASSERT(receiverMac->types[0] == ST_CTS); + if (compressed) { + for (size_t i = 0; i < senderMac->types.size(); i++) + if (senderMac->types[i] == ST_BLOCKACK_REQ) { + ASSERT(senderMac->compressedFrames[i]); + ASSERT(senderMac->lengths[i] == B(24)); + } + for (size_t i = 0; i < receiverMac->types.size(); i++) + if (receiverMac->types[i] == ST_BLOCKACK) { + ASSERT(receiverMac->compressedFrames[i]); + ASSERT(receiverMac->lengths[i] == B(32)); + ASSERT(receiverMac->rates[i] == bps(6000000)); + } + } auto firstRtsEndpoint = senderMac->ends[0] + senderMac->durations[0]; if (scenario == 1) { ASSERT(firstRtsEndpoint - senderMac->starts[0] == SimTime(280, SIMTIME_US)); @@ -272,6 +335,12 @@ class RateProtectionTest : public cSimpleModule { ASSERT(receiverMac->types.size() == 2); ASSERT(receiverMac->durations[0] == 0); ASSERT(receiverMac->durations[1] == SimTime(32, SIMTIME_US)); + if (compressed) + for (size_t i = 0; i < 2; i++) { + ASSERT(receiverMac->compressedFrames[i]); + ASSERT(receiverMac->lengths[i] == B(32)); + ASSERT(receiverMac->rates[i] == bps(6000000)); + } } if (scenario == 6) { ASSERT(senderMac->types.size() == 2); @@ -279,7 +348,10 @@ class RateProtectionTest : public cSimpleModule { ASSERT(senderMac->ends[0] + senderMac->durations[0] - senderMac->starts[0] == SimTime(340, SIMTIME_US)); ASSERT(sender->acknowledgments == 1); } - std::cout << "Basic Block Ack, response conversion, and rate estimates passed.\n"; + if (compressed) + std::cout << "Compressed Block Ack protection and response duration checks passed.\n"; + else + std::cout << "Basic Block Ack, response conversion, and rate estimates passed.\n"; } }; Define_Module(RateProtectionTest); @@ -341,20 +413,22 @@ record-scalar-results = false *.radioMedium.propagation.propagationTime = 0s **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${scenario} >= 7 ? "ac" : "a" +**.bandName = ${scenario} >= 7 ? "5 GHz (mapped 20 MHz)" : "2.4 GHz" +**.centerFrequency = ${scenario} >= 7 ? 5.18GHz : 2.412GHz **.wlan[*].bitrate = 6Mbps **.wlan[*].mac.typename = "RateProtectionMac" -**.wlan[*].mac.scenario = ${scenario=0..6} +**.wlan[*].mac.scenario = ${scenario=0..9} **.wlan[*].mac.qosStation = true **.wlan[*].mac.rx.typename = "RateProtectionRx" **.wlan[*].mac.hcf.typename = "RateProtectionHcf" -**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == 0 || ${scenario} == 1 || ${scenario} == 5 -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == 0 || ${scenario} == 1 || ${scenario} == 5 || ${scenario} >= 7 +**.wlan[*].mac.hcf.blockAckVariant = ${scenario} >= 7 ? "compressed" : "basic" **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 1 **.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 4096B -*.sender.wlan[0].mac.hcf.rateControl.typename = ${rateControl="","","TraceAarf","TraceAarf","DecreaseRateControl","","" ! scenario} +*.sender.wlan[0].mac.hcf.rateControl.typename = ${rateControl="","","TraceAarf","TraceAarf","DecreaseRateControl","","","","","" ! scenario} *.sender.wlan[0].mac.hcf.rateControl.initialRate = 6Mbps *.sender.wlan[0].mac.hcf.rateControl.interval = 100us **.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = ${scenario} >= 2 && ${scenario} <= 4 ? -1bps : 6Mbps @@ -365,7 +439,7 @@ record-scalar-results = false **.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 0B **.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 **.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 -**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 0 ? 1024us : 0s +**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 0 || ${scenario} == 7 ? 1024us : 0s *.sender.wlan[0].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = "multiple" *.sender.wlan[0].address = "02:00:00:00:00:01" *.receiver.wlan[0].address = "02:00:00:00:00:02" @@ -386,3 +460,6 @@ record-scalar-results = false %contains: stdout Basic Block Ack, response conversion, and rate estimates passed. + +%contains: stdout +Compressed Block Ack protection and response duration checks passed. diff --git a/tests/module/Ieee80211RecipientNav_1.test b/tests/module/Ieee80211RecipientNav_1.test index c4812c86883..bb8a4008e40 100644 --- a/tests/module/Ieee80211RecipientNav_1.test +++ b/tests/module/Ieee80211RecipientNav_1.test @@ -5,8 +5,10 @@ Retain received NAV refusal and DCF local NAV updates for holder frames. Require zero HCF local NAV updates for holder frames. Check a Basic Block Ack response after an ADDBA exchange through production HCF. Require its positive Duration/ID field without a local NAV update. +Repeat the response checks with Compressed and valid supplied HT peer state. +The Compressed case uses a 32-byte response MPDU and retains the same NAV invariant. -%extraargs: -r 0..2 +%extraargs: -r 0..3 %file: RecipientNav.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -14,6 +16,8 @@ Require its positive Duration/ID field without a local NAV update. #include "inet/linklayer/ieee80211/mac/Rx.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class RecipientNavRx : public Rx { @@ -32,6 +36,9 @@ class RecipientNavMac : public Ieee80211Mac { std::vector durations; simtime_t addbaResponseEnd = -1; Ptr basicBlockAck; + Ptr compressedBlockAck; + units::values::b blockAckLength; + const physicallayer::IIeee80211Mode *blockAckMode = nullptr; void sendDownFrame(Packet *packet) override { auto header = packet->peekAtFront(); types.push_back(header->getType()); @@ -43,6 +50,11 @@ class RecipientNavMac : public Ieee80211Mac { } if (auto response = dynamicPtrCast(header)) basicBlockAck = response; + if (auto response = dynamicPtrCast(header)) { + compressedBlockAck = response; + blockAckLength = packet->getDataLength(); + blockAckMode = packet->getTag()->getMode(); + } Ieee80211Mac::sendDownFrame(packet); } void injectBlockAckSetup() { @@ -58,12 +70,33 @@ class RecipientNavMac : public Ieee80211Mac { request->setStartingSequenceNumber(SequenceNumberCyclic(0)); injectBlockAckFrame(request); } - void injectBlockAckRequest() { + void prepareCompressedPeer() { + Enter_Method("prepareCompressedPeer"); + const MacAddress peer("02:00:00:00:00:02"); + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + auto radio = check_and_cast(getParentModule()->getSubmodule("radio")); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + ASSERT(mib->findPeerHtState(peer) && mib->findPeerHtState(peer)->valid); + } + void injectBlockAckRequest(bool compressed = false) { Enter_Method("injectBlockAckRequest"); - auto request = makeShared(); + Ptr request; + if (compressed) { + auto bar = makeShared(); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request = bar; + } + else { + auto bar = makeShared(); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request = bar; + } request->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); - request->setTidInfo(5); - request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setDurationField(SimTime(500, SIMTIME_US)); injectBlockAckFrame(request); } @@ -77,7 +110,7 @@ class RecipientNavMac : public Ieee80211Mac { header->setReceiverAddress(getAddress()); auto packet = new Packet("received-block-ack-exchange", header); packet->addTag()->setMode( - physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000))); + physicallayer::Ieee80211ModeSet::getModeSet(mib->isHtOperationSupported() ? "n(mixed-2.4Ghz)" : "a")->getMode(bps(6000000))); auto trailer = makeShared(); trailer->setFcsMode(FCS_DECLARED_CORRECT); packet->insertAtBack(trailer); @@ -140,8 +173,10 @@ class RecipientNavTest : public cSimpleModule { public: RecipientNavTest() : cSimpleModule(65536) {} protected: - void checkBasicBlockAck(RecipientNavMac *mac, RecipientNavRx *rx) { - auto mode = physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000)); + void checkBlockAck(RecipientNavMac *mac, RecipientNavRx *rx, bool compressed) { + auto mode = physicallayer::Ieee80211ModeSet::getModeSet(compressed ? "n(mixed-2.4Ghz)" : "a")->getMode(bps(6000000)); + if (compressed) + mac->prepareCompressedPeer(); mac->injectBlockAckSetup(); for (int i = 0; i < 100 && mac->addbaResponseEnd < SIMTIME_ZERO; i++) wait(SimTime(10, SIMTIME_US)); @@ -154,27 +189,40 @@ class RecipientNavTest : public cSimpleModule { ASSERT(rx->isMediumFree()); auto updatesBeforeResponse = rx->localUpdates; auto framesBeforeResponse = mac->types.size(); - mac->injectBlockAckRequest(); + mac->injectBlockAckRequest(compressed); wait(SimTime(350, SIMTIME_US)); ASSERT(mac->types.size() == framesBeforeResponse + 1); - ASSERT(mac->basicBlockAck != nullptr); - ASSERT(mac->basicBlockAck->getReceiverAddress() == MacAddress("02:00:00:00:00:02")); - ASSERT(mac->basicBlockAck->getTidInfo() == 5); - ASSERT(mac->basicBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + Ptr response; + if (compressed) { + ASSERT(mac->compressedBlockAck != nullptr && mac->basicBlockAck == nullptr); + ASSERT(mac->compressedBlockAck->getTidInfo() == 5); + ASSERT(mac->compressedBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(mac->compressedBlockAck->getBlockAckBitmap().getSize() == 64); + ASSERT(mac->blockAckLength == B(32)); + ASSERT(mac->blockAckMode == mode && mac->blockAckMode->getHtMcsIndex() < 0); + response = mac->compressedBlockAck; + } + else { + ASSERT(mac->basicBlockAck != nullptr); + ASSERT(mac->basicBlockAck->getTidInfo() == 5); + ASSERT(mac->basicBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + response = mac->basicBlockAck; + } + ASSERT(response->getReceiverAddress() == MacAddress("02:00:00:00:00:02")); // IEEE Std 802.11-2024, 9.2.5.6: subtract SIFS and response airtime from the request duration. - auto expectedDuration = SimTime(500, SIMTIME_US) - mode->getSifsTime() - mode->getDuration(LENGTH_BASIC_BLOCKACK); + auto expectedDuration = SimTime(500, SIMTIME_US) - mode->getSifsTime() - mode->getDuration(compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); ASSERT(expectedDuration > SIMTIME_ZERO); - ASSERT(mac->basicBlockAck->getDurationField() == expectedDuration); + ASSERT(response->getDurationField() == expectedDuration); ASSERT(rx->localUpdates == updatesBeforeResponse); ASSERT(rx->isMediumFree()); - std::cout << "Basic Block Ack recipient NAV checks passed.\n"; + std::cout << (compressed ? "Compressed" : "Basic") << " Block Ack recipient NAV checks passed.\n"; } void activity() override { wait(SimTime(1, SIMTIME_MS)); auto mac = check_and_cast(getModuleByPath("^.host.wlan[0].mac")); auto rx = check_and_cast(mac->getSubmodule("rx")); - if (par("basicBlockAckOnly").boolValue()) { - checkBasicBlockAck(mac, rx); + if (par("basicBlockAckOnly").boolValue() || par("compressedBlockAckOnly").boolValue()) { + checkBlockAck(mac, rx, par("compressedBlockAckOnly").boolValue()); std::cout << "Recipient NAV production checks passed.\n"; return; } @@ -231,6 +279,7 @@ simple RecipientNavRx extends Rx { simple RecipientNavTest extends SimpleModule { parameters: bool basicBlockAckOnly = default(false); + bool compressedBlockAckOnly = default(false); @class(::inet::ieee80211::RecipientNavTest); } network RecipientNavNetwork { @@ -251,14 +300,15 @@ record-vector-results = false record-scalar-results = false **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${scenario} == "compressedBlockAck" ? "n(mixed-2.4Ghz)" : "a" **.wlan[*].bitrate = 6Mbps **.wlan[*].mac.typename = "RecipientNavMac" -**.wlan[*].mac.qosStation = ${scenario="hcf","dcf","blockAck"} != "dcf" +**.wlan[*].mac.qosStation = ${scenario="hcf","dcf","blockAck","compressedBlockAck"} != "dcf" **.wlan[*].mac.rx.typename = "RecipientNavRx" -**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == "blockAck" -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == "blockAck" || ${scenario} == "compressedBlockAck" +**.wlan[*].mac.hcf.blockAckVariant = ${scenario} == "compressedBlockAck" ? "compressed" : "basic" *.test.basicBlockAckOnly = ${scenario} == "blockAck" +*.test.compressedBlockAckOnly = ${scenario} == "compressedBlockAck" **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps @@ -287,3 +337,6 @@ Recipient NAV production checks passed. %contains: stdout Basic Block Ack recipient NAV checks passed. + +%contains: stdout +Compressed Block Ack recipient NAV checks passed. diff --git a/tests/protocol/wifi/11ac/Ac_BlockAck.test b/tests/protocol/wifi/11ac/Ac_BlockAck.test index eeaa886371b..cd274cf2543 100644 --- a/tests/protocol/wifi/11ac/Ac_BlockAck.test +++ b/tests/protocol/wifi/11ac/Ac_BlockAck.test @@ -1,13 +1,19 @@ %description: 802.11ac Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] +Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. + +%extraargs: -r 0..1 %file: Ac_BlockAck.cc #include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { namespace protocoltest { +using namespace inet::ieee80211; + // Block Ack session on 11ac: STA sends ADDBA Request (type 13), // then BlockAckReq (type 24), AP replies with BlockAck (type 25). Define_ProtocolTest(wifi_11ac_block_ack) @@ -18,21 +24,63 @@ Define_ProtocolTest(wifi_11ac_block_ack) .describe("ADDBA action frame from sta1").within(2.5)) .once(on("sta1.wlan[0].mac").signal("packetSentToLower") .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) .describe("BlockAckReq from sta1").within(1.0)) .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (int i = 0; i < 64; i++) + for (unsigned int j = 0; j < ba->getBlockAckBitmap(i).getSize(); j++) + if (ba->getBlockAckBitmap(i).getBit(j)) return true; + return false; + }) .describe("BlockAck received at sta1").within(0.02)); } +Define_ProtocolTest(wifi_11ac_compressed_exchange) +{ + return ProtocolTest("wifi_11ac_compressed_exchange") + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 13") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto response = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + return response && response->getStatusCode() == 0 && response->getBlockAckPolicy(); + }) + .describe("successful immediate ADDBA response").within(2.5)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) + .describe("Compressed BAR from sta1").within(1.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (unsigned int i = 0; i < ba->getBlockAckBitmap().getSize(); i++) + if (ba->getBlockAckBitmap().getBit(i)) return true; + return false; + }) + .describe("Compressed Block Ack with positive receipt evidence").within(0.02)); +} + } // namespace protocoltest } // namespace inet %inifile: test.ini [General] -*.tester.testName = "wifi_11ac_block_ack" +*.tester.testName = ${test="wifi_11ac_block_ack","wifi_11ac_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -50,3 +98,6 @@ include ../../ini/_base.ini %contains: stdout PROTOCOLTEST wifi_11ac_block_ack: PASS + +%contains: stdout +PROTOCOLTEST wifi_11ac_compressed_exchange: PASS diff --git a/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test b/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test new file mode 100644 index 00000000000..dd86abc74e5 --- /dev/null +++ b/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test @@ -0,0 +1,186 @@ +%description: +Check runtime Compressed fallback under a controlled peer HT capability gap. +Simplified management supplies static association and valid HT state after initialization. +Before DATA, the fixture removes peer HT records at both ends of both wireless hops. +The fixture retains association, local HT support, ac operation, and Compressed Block Ack support. +During traffic, require delivered Normal Ack QoS DATA without ADDBA, BAR, or Block Ack. +Check absent peer HT records during traffic and at the final audit. +This fixture does not establish an on-air association exchange or a limitation of all ac stations. + +%file: Ac_BlockAckFallback.cc +#include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" + +namespace inet::protocoltest { +using namespace inet::ieee80211; + +class FallbackHcf : public Hcf { + public: + bool hasNoAgreements() const { + return originatorBlockAckAgreementHandler->getAgreements().empty() && + recipientBlockAckAgreementHandler->getAgreements().empty(); + } +}; +Define_Module(FallbackHcf); + +class BlockAckFallbackAudit : public cSimpleModule, public cListener { + protected: + Ieee80211Mib *ap = nullptr; + Ieee80211Mib *sta1 = nullptr; + Ieee80211Mib *sta2 = nullptr; + std::vector hcfs; + std::map macs; + bool removed = false; + bool completed = false; + int delivered = 0; + int sentFirstHop = 0; + int receivedFirstHop = 0; + int sentSecondHop = 0; + int receivedSecondHop = 0; + simsignal_t sentSignal; + simsignal_t receivedSignal; + simsignal_t appSignal; + + void checkAssociation() const { + for (auto sta : {sta1, sta2}) { + ASSERT(sta->bssStationData.isAssociated); + ASSERT(sta->bssData.bssid == ap->address); + auto it = ap->bssAccessPointData.stations.find(sta->address); + ASSERT(it != ap->bssAccessPointData.stations.end() && it->second == Ieee80211Mib::ASSOCIATED); + } + for (auto mib : {ap, sta1, sta2}) + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + for (auto hcf : hcfs) { + ASSERT(hcf->par("isBlockAckSupported").boolValue()); + ASSERT(std::string(hcf->par("blockAckVariant").stringValue()) == "compressed"); + ASSERT(hcf->hasNoAgreements()); + } + } + void checkGap() const { + checkAssociation(); + ASSERT(ap->findPeerHtState(sta1->address) == nullptr); + ASSERT(ap->findPeerHtState(sta2->address) == nullptr); + ASSERT(sta1->findPeerHtState(ap->address) == nullptr); + ASSERT(sta2->findPeerHtState(ap->address) == nullptr); + } + void initialize() override { + sentSignal = registerSignal("packetSentToLower"); + receivedSignal = registerSignal("packetReceivedFromLower"); + appSignal = registerSignal("packetReceived"); + scheduleAt(SIMTIME_ZERO, new cMessage("remove")); + scheduleAt(SimTime(1100, SIMTIME_MS), new cMessage("audit")); + } + void handleMessage(cMessage *message) override { + bool remove = message->isName("remove"); + delete message; + if (remove) { + ap = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + sta1 = check_and_cast(getModuleByPath("^.sta1.wlan[0].mib")); + sta2 = check_and_cast(getModuleByPath("^.sta2.wlan[0].mib")); + for (const char *name : {"ap", "sta1", "sta2"}) { + auto wlan = getParentModule()->getSubmodule(name)->getSubmodule("wlan", 0); + ASSERT(std::string(wlan->par("opMode").stringValue()) == "ac"); + auto mac = check_and_cast(wlan->getSubmodule("mac")); + macs[name] = mac; + hcfs.push_back(check_and_cast(mac->getSubmodule("hcf"))); + mac->subscribe(sentSignal, this); + mac->subscribe(receivedSignal, this); + } + checkAssociation(); + for (auto sta : {sta1, sta2}) { + auto atAp = ap->findPeerHtState(sta->address); + auto atSta = sta->findPeerHtState(ap->address); + ASSERT(atAp && atAp->valid && atSta && atSta->valid); + } + for (auto sta : {sta1, sta2}) { + ap->removePeerHtCapabilities(sta->address); + sta->removePeerHtCapabilities(ap->address); + } + getModuleByPath("^.sta2.app[0]")->subscribe(appSignal, this); + removed = true; + checkGap(); + } + else { + ASSERT(removed); + checkGap(); + ASSERT(delivered >= 5); + ASSERT(sentFirstHop >= 5 && receivedFirstHop >= 5); + ASSERT(sentSecondHop >= 5 && receivedSecondHop >= 5); + completed = true; + std::cout << "Absent peer HT fallback: Normal Ack delivered=" << delivered + << " firstHop=" << receivedFirstHop << " secondHop=" << receivedSecondHop << "\n"; + emit(registerSignal("fallbackAuditPassed"), intval_t(1)); + } + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *) override { + ASSERT(removed); + checkGap(); + auto packet = check_and_cast(object); + if (signal == appSignal) { + delivered++; + return; + } + auto header = packet->peekAtFront(); + ASSERT(!dynamicPtrCast(header)); + ASSERT(!dynamicPtrCast(header)); + ASSERT(header->getType() != ST_BLOCKACK_REQ && header->getType() != ST_BLOCKACK); + auto data = dynamicPtrCast(header); + if (!data) + return; + ASSERT(data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == NORMAL_ACK); + bool firstHop = data->getTransmitterAddress() == sta1->address && data->getReceiverAddress() == ap->address; + bool secondHop = data->getTransmitterAddress() == ap->address && data->getReceiverAddress() == sta2->address; + if (signal == sentSignal) { + sentFirstHop += firstHop && source == macs.at("sta1"); + sentSecondHop += secondHop && source == macs.at("ap"); + } + else if (signal == receivedSignal) { + receivedFirstHop += firstHop && source == macs.at("ap"); + receivedSecondHop += secondHop && source == macs.at("sta2"); + } + } + void finish() override { ASSERT(completed); } +}; +Define_Module(BlockAckFallbackAudit); + +Define_ProtocolTest(wifi_11ac_blockack_fallback) +{ + return ProtocolTest("wifi_11ac_blockack_fallback") + .once(on("audit").signal("fallbackAuditPassed") + .describe("Normal Ack delivery with absent peer HT state on both hops").within(1.2)); +} +} + +%file: FallbackNetwork.ned +import inet.protocoltest.wifi.WifiInfraNetwork; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +module FallbackHcf extends Hcf { parameters: @class(::inet::protocoltest::FallbackHcf); } +simple BlockAckFallbackAudit { parameters: @class(::inet::protocoltest::BlockAckFallbackAudit); } +network FallbackNetwork extends WifiInfraNetwork { + submodules: + audit: BlockAckFallbackAudit; +} + +%inifile: test.ini +[General] +network = FallbackNetwork +*.tester.testName = "wifi_11ac_blockack_fallback" +*.ap.wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified" +*.sta*.wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified" +*.sta*.wlan[*].agent.typename = "" +*.sta*.wlan[*].mgmt.accessPointAddress = "ap" +**.wlan[*].mac.hcf.typename = "FallbackHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +*.sta1.app[0].sendInterval = 0.1ms +*.sta1.app[0].stopTime = 1.01s +include ../../ini/_ac.ini +include ../../ini/_base.ini +**.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 10000B +**.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 100 +**.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 + +%contains: stdout +PROTOCOLTEST wifi_11ac_blockack_fallback: PASS diff --git a/tests/protocol/wifi/11n/N_BlockAck.test b/tests/protocol/wifi/11n/N_BlockAck.test index d5ab66019fe..98f60e3b1d7 100644 --- a/tests/protocol/wifi/11n/N_BlockAck.test +++ b/tests/protocol/wifi/11n/N_BlockAck.test @@ -1,13 +1,19 @@ %description: 802.11n Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] +Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. + +%extraargs: -r 0..1 %file: N_BlockAck.cc #include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { namespace protocoltest { +using namespace inet::ieee80211; + // Block Ack session flow: STA sends ADDBA Request (action frame, type 13), // AP responds with ADDBA Response (type 13), then after enough frames are // queued, STA sends BlockAckReq (type 24) and AP sends BlockAck (type 25). @@ -19,21 +25,63 @@ Define_ProtocolTest(wifi_11n_block_ack) .describe("ADDBA action frame from sta1").within(2.5)) .once(on("sta1.wlan[0].mac").signal("packetSentToLower") .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) .describe("BlockAckReq from sta1").within(1.0)) .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (int i = 0; i < 64; i++) + for (unsigned int j = 0; j < ba->getBlockAckBitmap(i).getSize(); j++) + if (ba->getBlockAckBitmap(i).getBit(j)) return true; + return false; + }) .describe("BlockAck received at sta1").within(0.02)); } +Define_ProtocolTest(wifi_11n_compressed_exchange) +{ + return ProtocolTest("wifi_11n_compressed_exchange") + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 13") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto response = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + return response && response->getStatusCode() == 0 && response->getBlockAckPolicy(); + }) + .describe("successful immediate ADDBA response").within(2.5)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) + .describe("Compressed BAR from sta1").within(1.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (unsigned int i = 0; i < ba->getBlockAckBitmap().getSize(); i++) + if (ba->getBlockAckBitmap().getBit(i)) return true; + return false; + }) + .describe("Compressed Block Ack with positive receipt evidence").within(0.02)); +} + } // namespace protocoltest } // namespace inet %inifile: test.ini [General] -*.tester.testName = "wifi_11n_block_ack" +*.tester.testName = ${test="wifi_11n_block_ack","wifi_11n_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -51,3 +99,6 @@ include ../../ini/_base.ini %contains: stdout PROTOCOLTEST wifi_11n_block_ack: PASS + +%contains: stdout +PROTOCOLTEST wifi_11n_compressed_exchange: PASS diff --git a/tests/protocol/wifi/11n/N_CompressedBa.test b/tests/protocol/wifi/11n/N_CompressedBa.test index 8d7910bb180..ffc72bf4215 100644 --- a/tests/protocol/wifi/11n/N_CompressedBa.test +++ b/tests/protocol/wifi/11n/N_CompressedBa.test @@ -1,5 +1,5 @@ %description: -802.11n Block Ack received at sta1 with non-empty bitmap (acknowledges at least one frame). [Required] +802.11n Compressed Block Ack received at sta1 with a positive receipt bit. [Required] %file: N_CompressedBa.cc @@ -11,10 +11,6 @@ namespace protocoltest { using namespace inet::ieee80211; -// INET sends Ieee80211BasicBlockAck (not CompressedBlockAck) for all block ack sessions. -// The Basic Block Ack carries a 64x16-bit bitmap acknowledging MPDUs. -// We assert that a BasicBlockAck is received and its bitmap has at least one bit set, -// confirming the AP has positively acknowledged at least one received data MPDU. Define_ProtocolTest(wifi_11n_compressed_ba) { return ProtocolTest("wifi_11n_compressed_ba") @@ -22,18 +18,14 @@ Define_ProtocolTest(wifi_11n_compressed_ba) .filterPacket("ieee80211mac.type == 25") .filterEvent([](const MatchContext& c) { if (!c.event.packet) return false; - auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); if (!ba) return false; - // At least one bit set means at least one MPDU was acknowledged. - for (int i = 0; i < 64; i++) { - const auto& bv = ba->getBlockAckBitmap(i); - for (unsigned int j = 0; j < bv.getSize(); j++) { - if (bv.getBit(j)) return true; - } - } + const auto& bitmap = ba->getBlockAckBitmap(); + for (unsigned int i = 0; i < bitmap.getSize(); i++) + if (bitmap.getBit(i)) return true; return false; }) - .describe("a BasicBlockAck with at least one acknowledged MPDU").within(2.5)); + .describe("a Compressed Block Ack with a positive receipt bit").within(2.5)); } } // namespace protocoltest @@ -44,7 +36,7 @@ Define_ProtocolTest(wifi_11n_compressed_ba) [General] *.tester.testName = "wifi_11n_compressed_ba" *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = "compressed" *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms diff --git a/tests/unit/Ieee80211BlockAckScheduling_1.test b/tests/unit/Ieee80211BlockAckScheduling_1.test index c0fd4049ab9..afeb6c4f616 100644 --- a/tests/unit/Ieee80211BlockAckScheduling_1.test +++ b/tests/unit/Ieee80211BlockAckScheduling_1.test @@ -138,6 +138,48 @@ choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 2); ASSERT(std::get<1>(choice) == SequenceNumberCyclic(100)); ASSERT(otherAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(4094)); +for (int start : {100, 4094}) { + for (int responseOffset : {0, 6, 100}) { + QosAckHandler compressedAck; + SchedulingFrames compressedFrames; + compressedFrames.configure(&compressedAck); + OriginatorBlockAckAgreement active(peer, 5, SequenceNumberCyclic(start), 64, false, false); + auto earlier = compressedFrames.add(peer, 5, (start + 2) % 4096); + auto requested = compressedFrames.add(peer, 5, (start + 4) % 4096); + auto unsent = compressedFrames.add(peer, 5, (start + 5) % 4096, false); + auto outsideWindow = compressedFrames.add(peer, 5, (start + 64) % 4096); + auto otherTid = compressedFrames.add(peer, 2, (start + 2) % 4096); + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + compressedAck.processTransmittedBlockAckReq(request, &active); + ASSERT(compressedAck.getQoSDataAckStatus(earlier) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(compressedAck.getQoSDataAckStatus(requested) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setTidInfo(5); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + responseOffset); + BitVector bitmap; + for (int bit = 0; bit < 64; bit++) + bitmap.appendBit(responseOffset == 0 && (bit == 2 || bit == 5)); + response->setBlockAckBitmap(bitmap); + compressedAck.processReceivedCompressedBlockAck(response, &active); + ASSERT(compressedAck.getQoSDataAckStatus(earlier) == (responseOffset == 0 ? + QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED : QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED)); + ASSERT(compressedAck.isEligibleToTransmit(requested)); + ASSERT(compressedAck.getQoSDataAckStatus(unsent) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(compressedAck.getQoSDataAckStatus(otherTid) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(compressedAck.getQoSDataAckStatus(outsideWindow) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + BitVector outsideBitmap; + for (int bit = 0; bit < 64; bit++) + outsideBitmap.appendBit(bit == 60); + response->setBlockAckBitmap(outsideBitmap); + compressedAck.processReceivedCompressedBlockAck(response, &active); + ASSERT(compressedAck.getQoSDataAckStatus(outsideWindow) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + } +} auto duration = SimTime(1, SIMTIME_US); EdcaTransmitLifetimeHandler lifetime(duration, duration, duration, duration); frames.setLifetimeHandler(&lifetime); From a7e5799530f4da467d8991b4f991e8f9b75e6ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:52:08 +0200 Subject: [PATCH 16/16] ieee80211: change: default HCF to immediate Compressed Block Ack Before this commit, a configuration without a profile selector uses Basic. The complete Compressed runtime requires an explicit selector. After this commit, HCF selects Compressed by default. Eligible HT links use immediate Compressed exchanges. Links without usable local or peer HT state retain Normal Ack. Explicit Basic retains its historical compatibility path and fragment bitmap. For example, an eligible n link without blockAckVariant now uses Compressed. The n and ac consumers cover explicit Basic, explicit Compressed, and the default. Migration cases cover capability fallback, profile conflict, and unsupported delayed policy. Existing Basic demonstrations retain explicit selectors. Execution records retain supported scope and evidence limits. A-MPDU, Multi-TID Block Ack, delayed Compressed policy, complete historical Basic conformance, and complete VHT transfer remain outside this result. Admission limits outside active Compressed agreements also remain outside the correction. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change | test whatsnew migration | ieee80211-compressed-blockack-replacement --- ...-compressed-block-ack-extension-points.rst | 2 +- doc/src/users-guide/ch-80211.rst | 2 +- ...ee80211-immediate-compressed-block-ack.txt | 2 +- ...e80211-compressed-blockack-s5-execution.md | 227 ++++++++++++++++++ ...0211-compressed-blockack-txop-admission.md | 121 ++++++++++ showcases/wireless/blockack/doc/index.rst | 2 +- .../mac/coordinationfunction/Hcf.ned | 2 +- tests/module/Ieee80211BlockAckDeferral_1.test | 1 + ...Ieee80211CompressedBlockAckExchange_1.test | 1 - ...eee80211CompressedBlockAckMigration_1.test | 179 ++++++++++++++ tests/protocol/wifi/11ac/Ac_BlockAck.test | 25 +- tests/protocol/wifi/11n/N_BlockAck.test | 25 +- 12 files changed, 575 insertions(+), 14 deletions(-) create mode 100644 plan/pending/ieee80211-compressed-blockack-s5-execution.md create mode 100644 plan/pending/ieee80211-compressed-blockack-txop-admission.md create mode 100644 tests/module/Ieee80211CompressedBlockAckMigration_1.test diff --git a/doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst b/doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst index 0f9cbdce7b5..abdf068b653 100644 --- a/doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst +++ b/doc/src/migration-guide/development/ieee80211-compressed-block-ack-extension-points.rst @@ -1,7 +1,7 @@ IEEE 802.11 Compressed Block Ack extension points ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Both endpoints must select the same profile. ADDBA does not negotiate this selector. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, local profile, and received profile. Configure the same selectors before a new run. +HCF defaults to ``mac.hcf.blockAckVariant = "compressed"``. Both endpoints must select the same profile. ADDBA does not negotiate this selector. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, local profile, and received profile. Configure the same selectors before a new run. Compressed agreements require usable local HT support and directional peer HT state. An HT PHY mode alone does not supply that state. Without it, HCF retains Normal Ack DATA. Explicit ``blockAckVariant = "basic"`` retains historical Basic compatibility without that HT requirement. Compressed agreements suppress fragmentation for their peer and TID. Fragmentation outside those agreements retains its existing threshold. diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index e6f09b57bd6..fa35d64e63b 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -706,7 +706,7 @@ After the final exchange, the originator sends DELBA when its agreement has no q The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and ``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size. ``maximumAllowedBufferSize`` defaults to 64. Both negotiated ``aMsduSupported`` flags control A-MSDU use; each defaults to ``true``. ``blockAckTimeoutValue`` defaults to ``0s``, which disables the inactivity timeout. Transfer completion and DATA lifetime expiry retain their separate teardown rules. -Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Block Ack support itself remains disabled by default. The response contains a 64-bit bitmap and occupies 32 bytes with FCS. Each positive bit acknowledges one unfragmented MPDU. The recipient retains receipt bits after delivery, so a later BAR can still obtain those positive bits. Missing receipt evidence produces zero. +HCF defaults to ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Block Ack support itself remains disabled by default. The response contains a 64-bit bitmap and occupies 32 bytes with FCS. Each positive bit acknowledges one unfragmented MPDU. The recipient retains receipt bits after delivery, so a later BAR can still obtain those positive bits. Missing receipt evidence produces zero. Compressed agreements require usable local HT support and directional peer HT state. Infrastructure association supplies that peer state for eligible HT links and the supported ``ac`` path. An HT radio mode alone does not supply peer capabilities. Without usable state, DATA uses Normal Ack. Local non-HT operation produces a warning when Block Ack support is enabled. HT ad hoc links without peer state retain Normal Ack. diff --git a/doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt b/doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt index 47d15c2b366..743bc0e1491 100644 --- a/doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt +++ b/doc/whatsnew/incompatible/ieee80211-immediate-compressed-block-ack.txt @@ -1,6 +1,6 @@ IEEE 802.11 immediate Compressed Block Ack -HCF supports explicit immediate Compressed Block Ack. Basic remains the default. Eligible HT infrastructure links and the supported ac path use accepted agreements and 32-byte Compressed responses with FCS. Links without usable local or peer HT state retain Normal Ack. Select blockAckVariant = "basic" at both endpoints for historical Basic compatibility. An HT PHY mode alone does not supply peer capability state. +HCF defaults to immediate Compressed Block Ack. Eligible HT infrastructure links and the supported ac path use accepted agreements and 32-byte Compressed responses with FCS. Links without usable local or peer HT state retain Normal Ack. Select blockAckVariant = "basic" at both endpoints for historical Basic compatibility. An HT PHY mode alone does not supply peer capability state. Both endpoints must select the same blockAckVariant because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Compressed rejects delayedAckPolicySupported = true in either agreement policy at initialization. diff --git a/plan/pending/ieee80211-compressed-blockack-s5-execution.md b/plan/pending/ieee80211-compressed-blockack-s5-execution.md new file mode 100644 index 00000000000..e89c9e74f7d --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-s5-execution.md @@ -0,0 +1,227 @@ +# S5 execution: Compressed Block Ack beside Basic compatibility + +S5 is complete through C1, C2, C3, and C4. The authorized TXOP correction removes the active Compressed timing blocker. HCF now defaults to Compressed. Explicit Basic compatibility remains available. + +The comparison parent is `75559a49661e33f727fa93a8c81c021596cfb4c9`. The active checkout is `/home/user/omnetpp_ws/inet-compressed-blockack-s5`. The maintainer requested S5 execution on 16 October 2026. That request authorizes C1 through C4 in the [replacement plan](ieee80211-compressed-blockack-replacement.md#compressed-replacement-steps). + +The checkout contained changes to `AGENTS.md` and four prerequisite plans before execution. It also contained an untracked planning history. This work preserves those changes. The pinned parent contains the S4 implementation. The [S4 execution record](ieee80211-compressed-blockack-s4-execution.md) supplies its earlier evidence and limits. + +## Implementation contract + +HCF owns the immutable profile and validates received frame formats before agreement updates. An active agreement identifies one peer and TID. A valid conflicting BAR stops the simulation because ADDBA does not negotiate the profile. Malformed input cannot cause that diagnostic, receipt updates, or agreement recovery. + +The recipient record owns explicit Compressed receipt bits and its scoreboard window. The reorder service owns delivery order and its separate window. Delivery must preserve receipt bits. An absent receipt produces zero, including positions before the first received DATA. Basic retains its fragment bitmap and existing compatibility behavior. + +The rate selector consumes the MIB rate sets. It selects a non-HT Compressed response and distinguishes initial BAR from later BAR. Frame sequences supply the actual request profile. Protection, timeouts, and duration calculations use the corresponding response length. Compressed responses contain 32 bytes with FCS; Basic responses contain 152 bytes. + +For an active Compressed agreement, the originator suppresses fragmentation for that peer and TID. The recipient rejects fragmented DATA before receipt or reorder updates. Normal Ack outside that agreement retains its existing fragmentation behavior. + +## Recipient interframe design + +The recipient must distinguish receipt evidence from the interframe relation. An agreement with start 100 and size four first receives Normal Ack DATA 100. Delivery advances the reorder start to 101. DATA 104 advances the scoreboard start to 101 and preserves receipt evidence for 100. A qualifying interframe relation permits delivery of 104 despite the gap. A false receipt bit or absent relation retains 104. + +Local receive endpoints alone cannot establish the ACK-to-DATA relation with propagation delay. The medium therefore supplies one copied snapshot of physical endpoints. The snapshot contains transmission identity, transmitter identity, transmission endpoints, and cached arrival endpoints. It retains no frame, transmission, arrival, or radio pointer. + +The PHY derives the interval at the current transmitter. For the same transmitter, it subtracts the preceding transmission end from the current transmission start. Otherwise, it subtracts the preceding signal's cached arrival end at the current transmitter. This calculation uses the propagation result that the medium already produced. It does not assume equal outward and return delay. + +HCF owns local event history. A pending candidate preserves the interval across PHY reception completion and later MAC delivery. A receiver-created transmission identifier connects those events. Unrelated receptions replace prior history. Failed, abandoned, and overlapping receptions must not preserve stale eligibility. Every local transmission clears prior history; completed ACK transmission can establish new history. + +SIFS recognition uses the reconstructed interval after signal extension. The current PHY model uses baseline timing without coverage-class adjustment. The permitted bound is one tenth of the slot time. Integer tick division rounds that bound inward. Physical propagation remains separate from this MAC timing parameter. + +RIFS recognition requires permitted HT operation, the same transmitter and RA, and no preceding SIFS response obligation. It uses the preceding PPDU endpoint without signal extension. Management must preserve the advertised RIFS Mode. VHT operation does not permit RIFS. This recipient rule adds no RIFS transmitter scheduler. + +The review found that ordinary HT transmission at 2.4 GHz omitted the six-microsecond signal extension. The correction belongs in the authoritative PHY duration. HCF must not add it again. Existing short-GI duration differences remain inherited PHY fidelity limits; they do not independently establish an interval error. + +## Standards evidence + +The standards source is IEEE Std 802.11-2024. The fresh local corpus is `/home/user/omnetpp_ws/inet/standards/processed`. The corpus lint output is `/tmp/inet-s5-standards-lint.json`. + +| Canonical node | Physical PDF pages | Requirement | +| --- | --- | --- | +| `ieee80211-2024:table:9-36` | 719–720 | BAR variant Type encoding. | +| `ieee80211-2024:table:9-37` | 722–723 | Block Ack variant Type encoding. | +| `ieee80211-2024:clause:10.25.6.3` | 2060–2061 | Full-state recipient receipt state. | +| `ieee80211-2024:clause:10.25.6.6.2.1` | 2065–2066 | Recipient delivery and interframe conditions. | +| `ieee80211-2024:clause:10.3.2.3.3` | 1889–1890 | SIFS definition and tolerance. | +| `ieee80211-2024:clause:10.3.2.3.2` | 1888–1889 | RIFS definition and conditions. | +| `ieee80211-2024:clause:10.22.5` | 1997–1998 | Coverage-class timing behavior. | +| `ieee80211-2024:table:19-25` | 3493–3494 | Band-specific HT signal extension. | + +These provisions are normative. The selected corpus clauses resolve uniquely. The review inspected the original PDF for Clause 11.38.1 on physical page 2886. That clause requires a VHT AP to clear RIFS Mode. The corpus result without children does not include all later prose in that clause. + +## Verification boundary + +The initial full debug build and subsequent focused rebuilds pass. A build supplies no exchange or timing proof. The focused wire fixture passes independent octet checks. The constructed serializer fixture passes 278 cases. The HCF input fixture passes both configured profiles with supplied recipient agreements. Those results do not establish negotiation or PHY interval recognition. + +Five initial protocol simulations pass across the selected n/ac fixtures. They retain Basic coverage and add actual Compressed exchanges. This initial boundary precedes complete infrastructure timing classification and final interframe execution. Later sections record those results and the authorized correction. + +An isolated observer reproduces a DATA continuation overrun in the pinned parent. With a one-millisecond TXOP, the third DATA ends 178 microseconds after the bound. The candidate reaches the same endpoints. The parent also reproduces the first observed A-MSDU overrun. These observations identify inherited DATA admission limits. They do not establish general TXOP conformance or excuse a new Compressed violation. + +No recorded fingerprint baseline changed. This record does not authorize a baseline change or publication. + +## Confirmed timing blocker + +The candidate before the TXOP correction introduced a failing supported case that the exact parent did not reproduce. General run 6 uses HT operation, 24 Mbit/s DATA, single protection, aggregation, a one-millisecond TXOP, and seed zero. That candidate transmits two later A-MSDUs beyond their TXOP bounds. Each overrun is 310 microseconds. The affected TXOP contains two DATA frames, so the initial-MSDU exception does not apply. + +The second DATA ends at 1.014426 seconds. Its TXOP bound is 1.014116 seconds. The violation exists before any Block Ack response. The aggregate contains 1532 bytes in both versions. The exact parent has no aggregate overrun in run 6. Parent run 14 demonstrates a similar admission defect under different conditions. That other case does not make candidate run 6 acceptable. + +A direct run 6 reproduction also fails with the final reviewed debug library. Its output is `/tmp/inet-s5-reviewed-run6.out`; its error is `/tmp/inet-s5-reviewed-run6.err`. That run stops at the first later A-MSDU violation, at 1.014426 seconds. The earlier non-fatal observer campaign records both violations. + +The inherited admission mechanism causes the new result. `HcfFs` tests positive remaining time before a later exchange. It does not establish that the complete exchange fits. HCF excludes Block Ack support from its existing complete-cost DATA guard. The corrected HT control airtime and Compressed responses change the trajectory. The comparison does not isolate their separate contributions. + +All sixteen initial parent and candidate runs deliver thirty payloads, with 21000 payload bytes per run. All observed BAR/Block Ack exchanges remain within their applicable bounds. Those successes do not establish DATA containment. An independent review rejected that candidate under the [plan's timing condition](ieee80211-compressed-blockack-replacement.md#airtime-acceptance-and-admission-dependencies). The later correction and C3 acceptance below supersede that rejection. + +The [separate correction proposal](ieee80211-compressed-blockack-txop-admission.md) addresses preparation and admission. A final transmit guard alone cannot solve the problem. Aggregation already deletes constituent frames before HCF selects the actual mode. An oversized aggregate can therefore remain prepared and fail every later attempt. The proposal must preserve progress through ordinary MSDUs and the permitted initial-MSDU exception. + +## Focused verification after source review + +The reviewed debug library has SHA-256 `1635793563ff1c0912a4a0e55a70507879af2a20ef83a0b88502562748a347db`. Its build log is `/tmp/inet-s5-reviewed-debug-build.log`. The interframe fixture and direct run 6 reproduction use this library. The earlier mixed-interface regression batch is discarded. + +The unit and legacy results below use their recorded fresh libraries before the final bounded PHY review corrections. Those corrections repair empty-cache lookup, remove an unused virtual, and expose a watch. The protocol campaign uses library `1c57b8f562dbeb5cddbba49b77a3a676042fea7aef9b9e5578e1980c63109f56`. These earlier results retain their recorded source boundaries; they are not new executions against the final relink. + +| Check | Result | Evidence | +| --- | --- | --- | +| Focused units | Nine fixtures pass. | `/tmp/inet-s5-final-unit.log` | +| HT Operation conversion | One fixture passes both RIFS permission values and operation equality checks. | `/tmp/inet-s5-ht-operation-unit.log` | +| Legacy and HCF input modules | Thirty fixtures pass across the main batch and two repaired-fixture reruns. | `/tmp/inet-s5-final-module.log`, `/tmp/inet-s5-repaired-module.log` | +| EDCA DATA lifetime modules | Nine fixtures pass. | `/tmp/inet-s5-lifetime-module.log` | +| Production interframe fixture | All 28 runs pass with seed zero. | `/tmp/inet-s5-interframe-final/` | +| Constructed serializers | All 278 cases pass. | `/tmp/inet-s5-serializer-roundtrip.log` | +| n/ac protocol fixtures | Five simulations pass with Basic and Compressed coverage. | `/tmp/inet-s5-exchange-protocol-final.log` | +| Lowest response mode | Two real infrastructure runs pass at 6 Mbit/s. | `/tmp/inet-s5-exchange-lowest-config.out` | + +The two legacy fixtures required valid synthetic frames. The Action-dispatch fixture now supplies the BAR trailer. The fragment-retirement fixture now supplies all 64 fragment bitmap entries. Their behavioral assertions remain intact. + +The interframe fixture supplies accepted agreements. It uses real PHY reception and HCF ACK completion. It verifies SIFS and RIFS bounds, sequence wrap, prior receipt truth, unrelated traffic, retirement, and cache expiry. Its BUSY control uses a signal below reception sensitivity but above energy detection. Its 168 malformed BAR receptions each leave zero pending candidates. Separate infrastructure tests establish real association and ADDBA; the interframe fixture does not claim negotiation coverage. + +The source reviewers report no remaining confirmed correctness defect in their bounded core and interframe scopes. Architecture gates pass for the changed MAC and PHY subtrees. Review supplies no runtime proof beyond the executed fixtures. + +## Extension contracts in this candidate + +These obligations describe the current candidate. They require migration guidance before publication. They do not change the C3 blocker or activate the default. + +| Contract | Obligation for a custom implementation | +| --- | --- | +| `IQosRateSelection::computeCompressedBlockAckReqMode()` | Select a legal non-HT BAR mode from the supplied initial-or-later TXOP position and current rate sets. | +| `IRecipientQosAckPolicy::computeCompressedBlockAckDurationField()` | Subtract SIFS and the actual 32-byte response airtime from the request reservation. Preserve the Basic method. | +| `IRecipientQosMacDataService::dataFrameReceived()` | Accept the new interframe argument. Preserve its meaning through recipient processing. A false value supplies no interval evidence. | +| `QoSContext` | Supply HCF's resolved profile to the exchange. Existing callers retain the Basic default unless they explicitly supply Compressed. | +| `RecipientBlockAckAgreement::setVariant()` | Set the profile during agreement creation. Do not change it during that agreement's lifetime. Compressed creation initializes explicit receipt state. | +| `IRadioMedium` | Implement the new snapshot and gap queries with copied physical facts. Unavailable transmissions or arrival endpoints produce an absent result. | +| `ICommunicationCache::findCachedArrival()` | Return a cached arrival without insertion, resizing, geometry recalculation, or other state changes. | +| `IIeee80211Mode::getSignalExtensionTime()` | Supply the nominal extension for the mode. `Ieee80211ModeBase` supplies zero; ERP and HT override it where necessary. | + +## Common regression result + +All nineteen selected rows match their recorded non-graphical fingerprints. The runner uses debug mode and the original configuration, run, time limit, and requested non-graphical ingredients. It completes in 182.173 seconds with exit status zero. The selected input is `/tmp/inet-s5-fingerprint-selection.csv`; the log is `/tmp/inet-s5-fingerprint.log`. + +The command runs from `tests/fingerprint`: + +```sh +./fingerprinttest -d \ + -m '(showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)|examples/(wireless/qos|adhoc/qos))' \ + -f tplx -f '~tNl' -f '~tND' -t 6 \ + /tmp/inet-s5-fingerprint-selection.csv +``` + +The graphical `tyf` ingredient remains unverified. No fingerprint baseline changed. These matches preserve the selected legacy regression evidence. They do not override the direct Compressed timing failure. + +These initial regression matches precede the separate TXOP correction and C4. No commit or publication occurred during this execution. + +## Build state before the authorized TXOP correction + +Both debug and release builds pass. The final release freshness check uses `make MODE=release -j6` from the repository root. Its log is `/tmp/inet-s5-reviewed-release-build.log`. The final debug build uses `make MODE=debug -j6`; its log is `/tmp/inet-s5-reviewed-debug-build.log`. The later NED edit corrects a comment and retains the Basic default. + +The separate TXOP proposal passed a consistency review as a scope request. On 2026-10-16, the maintainer authorized that correction and subsequent S5 completion. Source validation and independent review resolved the RTS boundary, lifetime ownership, and mode validation before implementation. The retained strict fixture preserves the inherited outside-agreement failure separately from active Compressed acceptance. + +## Authorized TXOP correction and C3 acceptance + +The [TXOP contract](ieee80211-compressed-blockack-txop-admission.md#resolved-implementation-contract) defines source ownership and each event boundary. The service creates duplicate previews without source removal. HCF selects one DATA mode, evaluates the aggregate, and evaluates an ordinary fallback with that same mode. Commitment transfers the same preview pointer only before DATA submission. Refusal preserves queue frames, deadlines, sequence state, and allocation state. + +RTS failure before DATA updates QSRC and the contention window without a provisional sequence counter. Original MSDU lifetimes bound repeated attempts. Expiry, capability loss, or changed response inputs cancel invalid preparation. Single protection reserves only the current exchange. A later BAR can require fresh contention. Basic and traffic outside active Compressed agreements retain their existing preparation and admission. + +Runtime verification found two effective selection callers that bypassed the first implementation. `RtsCtsFs` still extracted originals after admission. The multicast repeat predicate could also extract a prepared unicast successor before it ended the group sequence. Both callers now use the context-selected frame. Their focused regression cases preserve original queue state when no DATA submission occurs. + +The first retry policy required containment for all committed frames. A real BAR collision exposed a progress failure at 6 Mbit/s. One ordinary protected exchange needs 1.166 milliseconds under a nominal one-millisecond limit. Its first attempt qualifies for an exception, but conservative retry containment refused every later attempt. The implementation now uses the standard same-size single-MPDU exception with no earlier Data or Management frame. The supported path preserves committed MPDU size across retries. + +New previews clear Retry from the previous wireless hop. Committed retries derive Retry from local acknowledgment state. An oversized first-attempt A-MSDU still cannot use the retry exception. Internal collision commits only ordinary queued DATA before its retained recovery action. Multiple protection reserves the actual complete exchange for a permitted overrun. + +The corrected debug library has SHA-256 `0298dbd4c9625cd6f306865f5c1cfd0a3412230c6339b3a84289eb68eba713df`. The release library has SHA-256 `4b32335cdb2f558d60ada0761e46fde84c9879889e1c46b3c3f69bd11d9ae780`. Debug build 4 and the release build pass. Their logs are `/tmp/inet-s5-txop-debug-build-4.log` and `/tmp/inet-s5-txop-release-build.log`. + +The focused admission fixture passes all 24 cases with seed zero. It checks refusal, ordinary fallback, ownership, deadlines, sequence state, one mode query, and RTS cancellation. It also checks QSRC rollover, lifetime expiry, capability changes, internal collision, and multicast-to-unicast refusal. Cases 22 and 23 require actual ordinary and A-MSDU retries with unchanged size and DATA airtime above one millisecond. Evidence is `/tmp/inet-s5-admission24.out` and `/tmp/inet-s5-admission24.err`. + +All sixteen explicit-profile infrastructure cases pass the scoped timing assertions. The observer records agreement scope at DATA submission, before later teardown can change lookup results. Active Compressed checks include DATA with Normal Ack. No active later DATA or ACK overrun remains. All 98 matched BAR/Block Ack exchanges finish within their bounds. All 32 hop summaries report zero Block Ack overruns. + +The former run 6 failure now passes with zero DATA, ACK, or Block Ack overruns on both hops. Those hops transmit 23 and 11 active aggregates. The separate two-run lowest-response campaign records 29 actual 32-byte responses at 6 Mbit/s. Their airtime is 74 microseconds, with exactly 10 microseconds of SIFS. No Block Ack response exceeds its bound. + +The infrastructure observer separately records inherited violations outside active agreements. `StrictAllTiming` retains the original aggregate assertions and reproduces the pre-agreement aggregate failure in run 4. This acceptance establishes no timing compliance for those retained paths. The default change does not repair Basic or outside-agreement admission. + +The cleanup assertion permits only the idle `WAITING_FOR_SUCCESSOR` procedure with `SIMTIME_MAX` response deadline. Queues, in-progress frames, agreements, pending BAR state, and agreement timers must remain empty or inactive. That phase records a possible future setup after a completed Normal Ack transfer. It holds no unresolved exchange. + +Independent review accepts C3 for this scope and permits C4. The verdict is `/tmp/inet-s5-c3-acceptance-review.md`. Explicit-profile evidence is `/tmp/inet-s5-exchange-scoped-general16.out` and `/tmp/inet-s5-exchange-scoped-lowest2.out`. The scope table is `/tmp/inet-s5-exchange-scoped-general16.csv`. The unchanged strict failure remains in `/tmp/inet-s5-exchange-strict-all-inherited.out` and its `.err` file. + +## C4 default and migration verification + +`Hcf.ned` now defaults to Compressed. The infrastructure fixture omits the selector after C3 and requires actual Compressed frames. All sixteen default cases pass. Both lowest-response cases also pass after default activation. Evidence is `/tmp/inet-s5-default-exchange16.out` and `/tmp/inet-s5-default-lowest2.out`. + +All six external-style migration cases pass without an explicit selector. Legacy g and a links, and HT ad hoc links without peer records, deliver twelve UDP payloads through Normal Ack. Support-disabled controls produce no irrelevant warning. The fixture checks the resolved default, fallback diagnostics, absent ADDBA/BAR/Block Ack, and empty active state. Evidence is `/tmp/inet-s5-default-migration6.out`. + +All twelve initialization cases pass after default activation. They cover the omitted selector, explicit Basic, explicit Compressed, unknown values, and both delayed-policy guards. Evidence is `/tmp/inet-s5-default-variant12.out`. Retained Basic fragment, bitmap, and public API checks remain separate regression obligations. + +Both `N_BlockAck` and `Ac_BlockAck` also pass three consumer runs: explicit Basic, explicit Compressed, and the NED default. The third run uses bare INI `default`, which invokes OMNeT++ `acceptDefault()`. It verifies the resolved profile at all three HCF instances. Actual Compressed BAR/Block Ack and positive receipt bits complete that run. All six simulations and both diagnostic scripts pass. Evidence is `/tmp/inet-s5-default-consumer-protocol6.log` and `/tmp/inet-s5-default-consumer-{n3,ac3}.out`. + +Both final build freshness checks pass after the NED change. Their logs are `/tmp/inet-s5-c4-debug-build.log` and `/tmp/inet-s5-c4-release-build.log`. The C4 change affects the runtime NED default; the recorded debug and release library hashes remain unchanged. + +## Final regression evidence + +All final checks use the active checkout and seed zero where a simulation requires a seed. Debug mode retains runtime assertions. The release build supplies a separate compilation check. No fingerprint expectation changed. + +| Check | Final result | Evidence | +| --- | --- | --- | +| Unit fixtures | All ten pass. | `/tmp/inet-s5-c4-unit.log` | +| Module regression batch | Forty-one pass initially. The retained Basic deferral fixture passes after its explicit profile correction. All 42 selected fixtures therefore pass across the batch and focused rerun. | `/tmp/inet-s5-c4-module-regression.log`, `/tmp/inet-s5-c4-deferral-module.log` | +| Default infrastructure and migration | Both additional module fixtures pass: sixteen infrastructure cases and six migration cases. Both lowest-response cases pass. | `/tmp/inet-s5-default-module.log`, `/tmp/inet-s5-default-lowest2.out` | +| Serializer | All 278 constructed cases pass, with zero unexpected or stale coverage entries. The separate golden-octet unit also passes. | `/tmp/inet-s5-c4-serializer.log`, `/tmp/inet-s5-c4-unit.log` | +| Wi-Fi protocol files | Five supported files pass. Four deferred files retain expected failures. No unexpected result occurs. | `/tmp/inet-s5-default-plan-protocol9.log`, `/tmp/inet-s5-default-protocol-deferred.txt` | +| Fingerprint rows | All nineteen selected rows match in 153.811 seconds. | `/tmp/inet-s5-c4-fingerprint.log` | +| Architecture | All three scoped checks pass. | `/tmp/inet-s5-c4-architecture-mac.log`, `/tmp/inet-s5-c4-architecture-common.log`, `/tmp/inet-s5-c4-architecture-wifi.log` | + +The module union contains 44 distinct fixtures. It preserves Basic teardown, fragmentation, NAV, setup, association reset, response modes, and lifetime checks. It repeats the 28 physical interframe cases, 24 admission cases, and 76 scheduling cases after default activation. The scheduling fixture retains all original Basic cases. It adds actual Compressed shifted responses through active frame sequences and small-window arbitration. + +The recipient integration checks DATA 100, 102, and 101 through HCF. Delivery order is 100, 101, and 102. A subsequent BAR produces three positive receipt bits and zero holes after delivery. The six focused originator cases call the HCF control boundary directly. The scheduling fixture separately supplies actual response matching. These fixtures supply accepted agreements; the infrastructure and protocol fixtures prove negotiation separately. + +The legacy deferral fixture omitted a selector while it used non-HT mode `a`. The Compressed default correctly prevented its expected Basic setup. The fixture now selects Basic explicitly. All twelve original scenarios pass without assertion changes. This correction preserves the fixture's intended historical profile. + +The deferred protocol results remain explicit. `N_Ampdu` and `Ac_Ampdu` first miss an A-MPDU subframe. `Ac_MultiTidBa` passes both TID observations before it misses Multi-TID Block Ack. `WifiQosDelayedBa` observes a delayed request before recipient delayed acceptance fails. No expectation changed, and these results establish no support for those features. + +The interface gates report two MAC findings and three PHY findings. Their output is byte-identical to the pinned parent's output. The implicated source files remain unchanged. Logs are `/tmp/inet-s5-c4-interfaces-mac.log` and `/tmp/inet-s5-c4-interfaces-phy.log`, with corresponding `/tmp/inet-s5-parent-interfaces-*.log` files. This work adds no interface-gate finding. + +The final unit command runs from the checkout root: + +```sh +inet_run_unit_tests -m debug -f '(Ieee80211BlockAck(Wire|Scheduling|ActionWire)_1|Ieee80211CompressedBlockAck(Rate|Record)_1|Ieee80211(ResponseModeSelection|ResponseTimeout|HtModeSet|HtCapabilities|VhtHtCapabilities)_1)\.test$' +``` + +The module regression command runs from the checkout root: + +```sh +inet_run_module_tests -m debug -f 'Ieee80211(BlockAck.*|EdcaDataLifetime.*|PreparedCancellation_1|RecipientNav_1|Txnav_1|Hcf(SingleProtection_1|MultipleProtection_[12]|Header_1)|SingleProtectionGroup_1|VhtHtSupport_1|HtAssociation_1|Mgmt(StateCommit|RatePublication)_1|CompressedBlockAck(Input|Interframe|Admission|Originator)_1)\.test$' +inet_run_module_tests -m debug -f 'Ieee80211BlockAckDeferral_1\.test$' +inet_run_module_tests -m debug -f '(Ieee80211CompressedBlockAckExchange_1|Ieee80211CompressedBlockAckMigration_1|Ieee80211BlockAckVariant_1)\.test$' +``` + +The serializer and protocol commands run from the checkout root: + +```sh +inet_run_serializer_tests -m debug -f 'serializer_chunk_roundtrip\.test$' +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '(N_BlockAck|N_CompressedBa|N_Amsdu|N_Ampdu|Ac_BlockAck|Ac_BlockAckFallback|Ac_Ampdu|Ac_MultiTidBa|WifiQosDelayedBa)\.test$' +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '(N_BlockAck|Ac_BlockAck)\.test$' +``` + +The final fingerprint command repeats the nineteen-row command above from `tests/fingerprint`. It uses the original configurations, runs, time limits, and selected non-graphical ingredients. Graphical `tyf` remains unverified. No CSV or other recorded baseline changed. + +Current user and migration guides describe the default, fallback, profile conflict, fragmentation boundary, and added interface obligations. Release notes preserve the supported `ac` path and all four deferred protocol requirements. The Basic showcase retains its explicit profile, topology, and result rows. + +The evidence remains limited to the recorded deterministic cases and seed. Production HT catalogs provide no distinct legal alternate response for the tested 32-byte frame. The selector unit verifies that alternate-rate contract separately. The recipient prefix release remains an interpretation of ordered delivery together with the individual-MPDU release condition. This work establishes no complete historical Basic conformance or complete VHT transfer conformance. + +The implementation checks above precede the separate commit, push, and pull request phase. The user authorized that phase after implementation. The original local plan changes and untracked history remain outside the publication scope. diff --git a/plan/pending/ieee80211-compressed-blockack-txop-admission.md b/plan/pending/ieee80211-compressed-blockack-txop-admission.md new file mode 100644 index 00000000000..6664e786d49 --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-txop-admission.md @@ -0,0 +1,121 @@ +# TXOP prerequisite: admit Compressed DATA before destructive preparation + +This correction is complete and removes the active Compressed timing blocker. The [replacement plan](ieee80211-compressed-blockack-replacement.md#excluded-features-and-fragmentation) excludes complete-cost TXOP admission from its original scope. On 2026-10-16, the maintainer authorized this separate correction and subsequent S5 completion. The resolved contract below passed source validation and independent review before production edits. The [S5 execution record](ieee80211-compressed-blockack-s5-execution.md#authorized-txop-correction-and-c3-acceptance) supplies the final runtime evidence and scope limits. + +## Problem and required result + +The final S5 candidate sends two later A-MSDUs beyond their TXOP bounds in a supported case. The exact parent has no such violation in that case. The [S5 execution record](ieee80211-compressed-blockack-s5-execution.md#confirmed-timing-blocker) supplies the comparison and its limits. IEEE Std 802.11-2024, Clause 10.23.2.9 does not permit the observed continuation. + +The correction must establish the complete exchange cost before DATA commitment. An aggregate must fit its applicable bound. A later ordinary MSDU must also fit. The correction must retain the permitted initial unaggregated MSDU exception. It must not strand an oversized aggregate until lifetime expiry. + +A zero TXOP limit permits one SU PPDU with one MSDU or A-MSDU, plus its required protection and response. It does not mean zero permitted airtime. With a positive limit, the initial unaggregated MSDU exception permits at most one Data or Management frame. RTS and CTS airtime counts toward the exchange cost, but those Control frames do not increase that count. A same-size retransmission has a separate permitted exception under Clause 10.23.2.9; the implementation contract must state its retry policy. + +Hypothetical example: a TXOP has 200 microseconds left. The proposed aggregate needs 400 microseconds, while its first ordinary MSDU needs 150 microseconds. The service must commit that ordinary MSDU and leave the other MSDUs queued. If neither form qualifies, the sequence must end without queue removal or sequence assignment. + +The initial implementation scope covers active Compressed agreements. Explicit Basic and Normal Ack fallback outside an agreement retain their current preparation paths. This boundary avoids an unrelated compatibility repair. It does not claim that those retained paths satisfy every TXOP constraint. + +## Confirmed cause + +The [data service](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc) removes aggregation candidates before HCF selects the actual DATA mode. The aggregation builder deletes the constituent frames. The service replaces their lifetime records and assigns a sequence number. At that point, a final HCF refusal cannot recover the original queue form. + +The [frame sequence](../../src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc) tests whether positive TXOP time remains. That test does not prove that a complete later exchange fits. HCF excludes Block Ack support from its existing complete-cost DATA guard. A guard at final submission therefore needs a preparation correction as well. + +AARF and Onoe can change state when the rate selector queries them. Separate mode queries for a preview, RTS protection, and DATA submission can produce different decisions. The cost calculation must use the actual selected mode without extra rate-control queries. + +## Proposed ownership and sequence + +| Step | Responsible component and proposed action | Expected result and verification | +| --- | --- | --- | +| 1. Prepare without commitment. | The data service obtains borrowed candidates from the aggregation policy. It constructs a preview from duplicates. | Queue ownership, deadlines, and sequence counters remain unchanged. A focused fixture records them before and after refusal. | +| 2. Select one DATA mode. | HCF selects the mode once for this preparation. The frame-sequence context owns that decision and the preparation state. | Aggregate evaluation, ordinary-MSDU evaluation, protection, and submission use one mode decision. A test rate control counts queries. | +| 3. Calculate complete cost. | HCF derives the applicable IFS, DATA duration, protection, and immediate response from their existing owners. | Admission checks the TXOP bound and the applicable TXNAV bound. PHY mode objects supply durations. | +| 4. Select a permitted form. | HCF first evaluates the aggregate. If it fails, HCF evaluates the selected ordinary MSDU with the same mode. | An oversized aggregate does not cause permanent refusal. The ordinary MSDU can fit or qualify for the initial-MSDU exception. | +| 5. Defer when necessary. | The frame sequence ends normally when neither form qualifies. | The queue remains intact. Refusal causes no DATA retry outcome, sequence assignment, or aggregate lifetime replacement. | +| 6. Commit at a safe boundary. | The data service commits only the accepted form. HCF uses its retained mode for the actual exchange. | Extraction, lifetime replacement, and sequence assignment occur once. No committed aggregate waits indefinitely because its form cannot fit. | + +BAR arbitration must precede DATA preparation, as S4 requires. The correction adds no BAR forecast. A final BAR can obtain a later TXOP through the existing arbitration path. + +The existing final false-transmit result remains a safety check. The frame-sequence handler removes a refused step and ends the sequence without a transmission-failure result. Normal admission refusal must occur before destructive source commitment. + +## Boundary requirements + +RTS creates an event boundary between preparation and DATA submission. A capability update, lifetime expiry, or RTS failure can occur before DATA starts. The implementation contract must identify the owner of each preview and original frame throughout that interval. + +The resolved contract retains reversible preparation until the first DATA submission. A failed RTS or invalidated capability discards the preview and its selected mode. Original queue frames retain their existing deadlines and sequence state. A later attempt obtains a new preparation and mode decision. The source trace established that this boundary fits the existing ownership and lifetime contracts. + +The frame-sequence API must distinguish preparation refusal from an empty queue and a failed transmission. `InProgressFrames` must not convert those outcomes into a DATA retry. The integration must preserve S4's preview and BAR-first contracts. + +Admission refusal causes no DATA retry outcome. An actual RTS failure retains its required recovery behavior while the service discards reversible preparation. The implementation contract must keep those paths separate. + +For DATA under an active Block Ack agreement, an actual RTS failure updates QSRC and the contention window. QSRC is the short retry counter for an access category. Clause 10.23.2.12.1 applies its associated-RTS increment rule to MSDUs and A-MSDUs outside a Block Ack agreement. A preview must not create a sequence-keyed counter before sequence assignment. QSRC rollover therefore does not discard queued DATA; its lifetime still bounds repeated attempts. This changes INET's former per-frame RTS discard behavior on this scoped path. Basic and Normal Ack outside an agreement retain their existing recovery. + +At the preceding exchange completion, TXNAV admission compares complete exchange cost strictly below the remaining TXNAV. That cost excludes the leading IFS. Positive-limit TXOP containment includes the leading IFS and permits equality with the bound. Clauses 10.23.2.8 and 9.2.5.2 support these separate comparisons. The correction must not move the strict TXNAV comparison to a later reference that rejects an exact single-protection reservation. + +The correction must not retain an illegal mode after peer capabilities change. It must also avoid a second adaptive-rate query merely to validate an unchanged prepared decision. The implementation contract must specify that validation separately from mode selection. + +These are engineering checks for the approved scope. They do not require the maintainer to choose a cache format or callback signature. If source analysis needs a different behavior or a wider compatibility change, the owner must update this proposal first. + +## Resolved implementation contract + +The data service owns duplicate previews through `PreparedQosData`. Each preparation records original frame IDs and its source queue. It retains no original frame pointer across events. `prepareFrame(queue, allowAggregation)` creates one form without extraction. HCF first requests the aggregate form. If that form fails admission, HCF requests the ordinary form with the same DATA mode. + +`QoSContext` owns the selected preparation and its retained modes. HCF supplies a preparation callback with three outcomes: `LEGACY`, `READY`, and `DEFERRED`. `HcfFs` calls it after BAR arbitration and before extraction. Refusal ends the repeat normally. Legacy contexts retain their existing extraction route. + +`isPreparationValid()` checks source membership, eligibility, deadlines, next sequence, and A-MSDU permission. HCF separately validates every retained mode and response input through pure queries. These checks include mode-set changes. A changed input cancels preparation without another adaptive DATA query. HCF repeats validation before RTS and before DATA submission. + +`commitPreparedFrame()` removes the accepted sources only before first DATA submission. It transfers the same preview pointer to `InProgressFrames`. Ordinary commitment transfers the original absolute deadline. Aggregate commitment starts the existing A-MSDU lifetime. Commitment assigns one sequence number and allocation index. Frame-sequence steps retain valid borrowed pointers until their disposal. + +```mermaid +flowchart TD + Queue[Original MSDUs remain queued.] --> Preview[The service creates duplicate previews.] + Preview --> Admission{Does a permitted form qualify?} + Admission -->|No| Refuse[HCF ends the sequence without commitment.] + Admission -->|Yes| Protection{Does DATA require RTS?} + Protection -->|No| Validate{Are sources and retained modes valid?} + Protection -->|Yes| RTS[HCF submits RTS with the retained decision.] + RTS -->|CTS succeeds| Validate + RTS -->|CTS fails for valid sources| Recovery[Recovery updates QSRC and CW.] + RTS -->|Sources become invalid| Discard + Recovery --> Discard[Context disposal deletes the preview.] + Validate -->|No| Discard + Validate -->|Yes| Commit[The service commits the same preview pointer.] + Commit --> Data[HCF submits DATA with its retained mode.] +``` + +Expiry during CTS wait can remove originals safely because preparation stores their IDs. An invalid preparation ends without stale DATA or a synthetic RTS failure. A valid preparation with failed CTS causes one actual RTS recovery outcome. Context disposal deletes unused previews after step disposal. + +The scoped single-protection path reserves only its current exchange. It makes no forecast for unprepared successor DATA. A later BAR can contend through the existing S4 route. Multiple protection retains its reservation formula. This choice can increase contention and change simulation trajectories for active Compressed agreements. + +Committed retransmissions use the same-size single-MPDU exception from Clause 10.23.2.9 when no earlier Data or Management frame occurred in the TXOP. This exception can include a retransmitted A-MSDU. It does not permit an oversized first-attempt A-MSDU. The supported Compressed path has no fragmentation or A-MPDU operation. Its committed MPDU retains its size across retries; Retry and Duration field changes do not change that size. Basic and outside-agreement fallback retain their existing paths. + +A bounded LLDB observation established why conservative retry containment cannot satisfy the supported exchange. AP DATA at 6 Mbit/s needs a 1.166-millisecond protected exchange within a nominal one-millisecond TXOP. Its first ordinary transmission qualifies for the initial exception. A real BAR collision makes that DATA retry eligible. Conservative containment then refuses every attempt despite the permitted same-size exception. The root validated the required policy correction before source edits. Evidence is `/tmp/inet-s5-txop-diagnostic-lldb-cost.out`. + +Internal collision has a separate preparation entry. The existing HCF branch extracts DATA before it updates collision recovery. On the scoped queued path, HCF instead commits one ordinary preview before that existing recovery action. It must not create an aggregate before admission. Other queued MSDUs retain their ownership and deadlines. An unsent ordinary MSDU still qualifies for the first-transmission exception when its conditions hold. + +Multiple protection must reserve the actual exchange when a permitted initial ordinary MSDU or same-size retransmission exceeds a positive limit. The scoped path uses the existing helper's current-exchange form for that case. It must not reduce protection to zero merely because DATA exceeds the limit. The helper's reservation formula remains unchanged. + +The root validated this contract against the current source. An independent review found no unresolved blocking gap. This contract review supplies no runtime proof. Production implementation and focused verification follow this gate. + +## Proposed verification + +The direct reproduction uses `Ieee80211CompressedBlockAckExchange_1.test`, General run 6, seed zero. It selects HT operation, 24 Mbit/s DATA, single protection, aggregation, and a one-millisecond TXOP. The candidate must eliminate both 310-microsecond continuation overruns. The raw comparison remains in `/tmp/inet-s5-exchange-parent-candidate-comparison.json`. + +Add a focused preparation fixture at the proposed path `tests/module/Ieee80211CompressedBlockAckAdmission_1.test`. That fixture does not yet exist. Its cases must establish these results: + +- An initial aggregate that cannot fit becomes an ordinary MSDU before commitment. +- A permitted initial unaggregated MSDU can exceed the TXOP limit under the applicable exception. +- A smaller ordinary MSDU can replace an aggregate when it fits the remaining interval. +- Refusal preserves queue ownership, deadlines, and sequence counters when neither form qualifies. +- RTS failure, lifetime expiry, and capability changes discard invalid preparation without stale DATA transmission. +- RTS protection, DATA duration, and admission consume one selected mode. +- Basic controls preserve their existing behavior. + +Repeat S5's complete infrastructure campaign after the correction. Retain the lowest-response cases and exact BAR/Block Ack bounds. Repeat the affected lifetime, setup, frame-sequence, and fragmentation fixtures. Repeat the nineteen selected fingerprint rows without a baseline update. + +C3 can pass only after the supported corrected exchanges meet their applicable bounds. C4 and default activation must follow that result. This proposal grants no publication or fingerprint-baseline authority. + +## Execution result + +All 24 focused admission cases pass. All sixteen infrastructure cases pass the active Compressed timing assertions. Both lowest-response cases pass. Independent review accepts C3. C4 activates the default only after that decision and repeats its required checks. + +The former run 6 violation no longer occurs on either hop. Outside-agreement admission retains its inherited limits. `StrictAllTiming` preserves their separate reproduction. Basic settings and recorded fingerprint expectations remain unchanged. The final nineteen-row fingerprint campaign passes without a baseline update. diff --git a/showcases/wireless/blockack/doc/index.rst b/showcases/wireless/blockack/doc/index.rst index 2d6aa48f04e..b4808ed7c33 100644 --- a/showcases/wireless/blockack/doc/index.rst +++ b/showcases/wireless/blockack/doc/index.rst @@ -50,7 +50,7 @@ Block Acknowledgment in INET In INET, the block ack mechanism can be enabled by setting the :par:`isBlockAckSupported` parameter of the MAC's :ned:`Hcf` submodule to ``true`` (it is disabled by default). -This showcase selects ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. Basic remains the default. Other configurations can select Compressed explicitly. Compressed agreements require usable local and peer HT state. Without that state, DATA uses Normal Ack. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. +This showcase selects ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. HCF defaults to Compressed for other configurations. Compressed agreements require usable local and peer HT state. Without that state, DATA uses Normal Ack. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. Several submodules of :ned:`Hcf` are involved in the block ack mechanism. These modules have parameters controlling the block ack behavior. The following paragraphs diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index e38a6636622..19462280f07 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -29,7 +29,7 @@ module Hcf extends Module like IHcf { parameters: bool isBlockAckSupported = default(false); - string blockAckVariant = default("basic"); // Compressed requires usable HT state. Select Basic explicitly for historical compatibility. + string blockAckVariant = default("compressed"); // Compressed requires usable HT state. Select Basic explicitly for historical compatibility. string rxModule; string txModule; diff --git a/tests/module/Ieee80211BlockAckDeferral_1.test b/tests/module/Ieee80211BlockAckDeferral_1.test index 58f6614bc96..b1d38cbbfa9 100644 --- a/tests/module/Ieee80211BlockAckDeferral_1.test +++ b/tests/module/Ieee80211BlockAckDeferral_1.test @@ -384,6 +384,7 @@ record-vector-results = false **.wlan[*].mac.qosStation = true **.wlan[*].mac.hcf.typename = "DeferralHcf" **.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} != 4 +**.wlan[*].mac.hcf.blockAckVariant = "basic" **.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.typename = ${policy="OriginatorBlockAckAgreementPolicy","LongTimeoutPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy" ! scenario} **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = ${aggregation="","","","","","","","","","","","BasicMsduAggregationPolicy" ! scenario} **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 diff --git a/tests/module/Ieee80211CompressedBlockAckExchange_1.test b/tests/module/Ieee80211CompressedBlockAckExchange_1.test index 4ccb39e660a..2988432ef82 100644 --- a/tests/module/Ieee80211CompressedBlockAckExchange_1.test +++ b/tests/module/Ieee80211CompressedBlockAckExchange_1.test @@ -352,7 +352,6 @@ network ExchangeNetwork { %inifile: omnetpp.ini [General] -**.hcf.blockAckVariant = "compressed" network = ExchangeNetwork ned-path = .;../../../../src sim-time-limit = 1.3s diff --git a/tests/module/Ieee80211CompressedBlockAckMigration_1.test b/tests/module/Ieee80211CompressedBlockAckMigration_1.test new file mode 100644 index 00000000000..72db8ff5672 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckMigration_1.test @@ -0,0 +1,179 @@ +%description: +An external configuration enables Block Ack without an explicit profile. +Legacy g and a links, and an HT ad hoc link without peer HT records, must deliver twelve UDP payloads with Normal Ack. +Require a queued successor without ADDBA, BAR, or BA. Check profile and capability diagnostics with support enabled and disabled. + +%extraargs: -r 0..5 + +%postrun-command: python3 check-diagnostics.py + +%file: Migration.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet::ieee80211 { +class MigrationHcf : public Hcf { + public: + int normalData = 0; + int requests = 0; + int bars = 0; + int responses = 0; + bool queuedSuccessor = false; + bool compressed() const { return std::string(par("blockAckVariant").stringValue()) == "compressed"; } + bool clean() const { + return (!originatorBlockAckAgreementHandler || (originatorBlockAckAgreementHandler->getAgreements().empty() && + originatorBlockAckAgreementHandler->getProcedures().empty())) && + (!recipientBlockAckAgreementHandler || recipientBlockAckAgreementHandler->getAgreements().empty()); + } + protected: + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (!accepted) + return false; + requests += dynamicPtrCast(header) != nullptr; + bars += dynamicPtrCast(header) != nullptr; + responses += dynamicPtrCast(header) != nullptr; + if (auto data = dynamicPtrCast(header)) { + ASSERT(data->getAckPolicy() == NORMAL_ACK); + ASSERT(!mib->findPeerHtState(data->getReceiverAddress())); + normalData++; + } + return true; + } + void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& header, AccessCategory ac) override { + queuedSuccessor |= edca->getEdcaf(ac)->getPendingQueue()->getNumPackets() > 0; + Hcf::originatorProcessTransmittedDataFrame(packet, header, ac); + } +}; +Define_Module(MigrationHcf); + +class MigrationTest : public cSimpleModule, public cListener { + int received = 0; + int bytes = 0; + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); } + void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + auto packet = check_and_cast(value); + received++; + bytes += packet->getByteLength(); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + getModuleByPath("^.host2.app[0]")->subscribe(packetReceivedSignal, this); + scheduleAt(SimTime(99, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + ASSERT(received == 12 && bytes == 8400); + for (const char *path : {"^.host1.wlan[0].mac.hcf", "^.host2.wlan[0].mac.hcf"}) { + auto hcf = check_and_cast(getModuleByPath(path)); + ASSERT(hcf->compressed() && hcf->clean()); + ASSERT(hcf->requests == 0 && hcf->bars == 0 && hcf->responses == 0); + if (hcf->normalData > 0) + ASSERT(hcf->normalData >= 12 && hcf->queuedSuccessor); + } + ASSERT(check_and_cast(getModuleByPath("^.host1.wlan[0].mac.hcf"))->normalData >= 12); + std::cout << "Compressed default migration assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(MigrationTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.AdhocHost; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module MigrationHcf extends Hcf { parameters: @class(::inet::ieee80211::MigrationHcf); } +simple MigrationTest { parameters: string opMode; bool supported; @class(::inet::ieee80211::MigrationTest); } +network MigrationNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host1: AdhocHost; + host2: AdhocHost; + test: MigrationTest; +} + +%inifile: omnetpp.ini +[General] +network = MigrationNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = false +cmdenv-event-banners = false +**.mac.hcf.originatorBlockAckAgreementPolicy.cmdenv-log-level = debug +**.mac.hcf.cmdenv-log-level = info +**.cmdenv-log-level = off +record-vector-results = false +record-scalar-results = false +*.test.opMode = ${opMode="g(mixed)","a","n(mixed-2.4Ghz)"} +*.test.supported = ${supported=true,false} +*.host1.numApps = 1 +*.host1.app[0].typename = "UdpBasicApp" +*.host1.app[0].destAddresses = "host2" +*.host1.app[0].destPort = 1000 +*.host1.app[0].messageLength = 700B +*.host1.app[0].startTime = 10ms +*.host1.app[0].stopTime = 11.15ms +*.host1.app[0].sendInterval = 100us +*.host2.numApps = 1 +*.host2.app[0].typename = "UdpSink" +*.host2.app[0].localPort = 1000 +*.host*.ipv4.arp.typename = "GlobalArp" +*.host*.wlan[*].opMode = ${opMode} +*.host*.wlan[*].radio.bandName = ${opMode} == "a" ? "5 GHz (mapped 20 MHz)" : "2.4 GHz" +*.host*.wlan[*].radio.channelNumber = 0 +*.host*.wlan[*].bitrate = 6Mbps +*.host*.wlan[*].classifier.typename = "QosClassifier" +*.host*.wlan[*].mac.qosStation = true +*.host*.wlan[*].mac.hcf.typename = "MigrationHcf" +*.host*.wlan[*].mac.hcf.isBlockAckSupported = ${supported} +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +*.host*.wlan[*].radio.receiver.energyDetection = -105dBm +*.host*.wlan[*].radio.receiver.sensitivity = -105dBm +*.host*.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.host1.mobility.initialX = 0m +*.host2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%file: check-diagnostics.py +from pathlib import Path +import re + +text = Path("test.out").read_text() +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", text)[1:] +assert len(runs) == 6, len(runs) +warning = "Compressed Block Ack has no usable local HT support" +profile = "Block Ack profile: compressed" +for run in runs: + ht = '$opMode="n(mixed-2.4Ghz)"' in run + supported = "$supported=true" in run + assert "Compressed default migration assertions passed." in run + assert run.count(profile) == (2 if supported else 0), run + assert run.count(warning) == (2 if supported and not ht else 0), run + if supported: + reason = "peer HT state is absent" if ht else "local HT state is absent" + assert f"Compressed Block Ack setup refused: {reason}; DATA uses Normal Ack." in run, run + else: + assert "Compressed Block Ack setup refused:" not in run, run +print("Compressed default migration diagnostics passed.") + +%contains: stdout +Compressed default migration assertions passed. diff --git a/tests/protocol/wifi/11ac/Ac_BlockAck.test b/tests/protocol/wifi/11ac/Ac_BlockAck.test index cd274cf2543..e6407799617 100644 --- a/tests/protocol/wifi/11ac/Ac_BlockAck.test +++ b/tests/protocol/wifi/11ac/Ac_BlockAck.test @@ -1,8 +1,11 @@ %description: 802.11ac Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] -Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. +Check explicit Basic, explicit Compressed, and the NED default with concrete BAR/BA types and positive receipt bits. +The third run uses the unquoted INI keyword default. OMNeT++ accepts the parameter's NED default for this keyword. -%extraargs: -r 0..1 +%extraargs: -r 0..2 + +%postrun-command: python3 check-default.py %file: Ac_BlockAck.cc @@ -78,9 +81,9 @@ Define_ProtocolTest(wifi_11ac_compressed_exchange) %inifile: test.ini [General] -*.tester.testName = ${test="wifi_11ac_block_ack","wifi_11ac_compressed_exchange"} +*.tester.testName = ${test="wifi_11ac_block_ack","wifi_11ac_compressed_exchange","wifi_11ac_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed",default ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -96,6 +99,20 @@ include ../../ini/_base.ini **.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 *.sta*.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +%file: check-default.py +from pathlib import Path +import re + +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", Path("test.out").read_text())[1:] +assert len(runs) == 3, len(runs) +for run, profile in zip(runs, ["basic", "compressed", "default"]): + assert f'$profile={profile if profile == "default" else chr(34) + profile + chr(34)}' in run + resolved = "basic" if profile == "basic" else "compressed" + assert run.count(f"Block Ack profile: {resolved}") == 3 + test = "wifi_11ac_block_ack" if profile == "basic" else "wifi_11ac_compressed_exchange" + assert f"PROTOCOLTEST {test}: PASS" in run +print("Basic, explicit Compressed, and NED default exchanges passed.") + %contains: stdout PROTOCOLTEST wifi_11ac_block_ack: PASS diff --git a/tests/protocol/wifi/11n/N_BlockAck.test b/tests/protocol/wifi/11n/N_BlockAck.test index 98f60e3b1d7..0ae29c49cad 100644 --- a/tests/protocol/wifi/11n/N_BlockAck.test +++ b/tests/protocol/wifi/11n/N_BlockAck.test @@ -1,8 +1,11 @@ %description: 802.11n Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] -Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. +Check explicit Basic, explicit Compressed, and the NED default with concrete BAR/BA types and positive receipt bits. +The third run uses the unquoted INI keyword default. OMNeT++ accepts the parameter's NED default for this keyword. -%extraargs: -r 0..1 +%extraargs: -r 0..2 + +%postrun-command: python3 check-default.py %file: N_BlockAck.cc @@ -79,9 +82,9 @@ Define_ProtocolTest(wifi_11n_compressed_exchange) %inifile: test.ini [General] -*.tester.testName = ${test="wifi_11n_block_ack","wifi_11n_compressed_exchange"} +*.tester.testName = ${test="wifi_11n_block_ack","wifi_11n_compressed_exchange","wifi_11n_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed",default ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -97,6 +100,20 @@ include ../../ini/_base.ini **.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 *.sta*.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +%file: check-default.py +from pathlib import Path +import re + +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", Path("test.out").read_text())[1:] +assert len(runs) == 3, len(runs) +for run, profile in zip(runs, ["basic", "compressed", "default"]): + assert f'$profile={profile if profile == "default" else chr(34) + profile + chr(34)}' in run + resolved = "basic" if profile == "basic" else "compressed" + assert run.count(f"Block Ack profile: {resolved}") == 3 + test = "wifi_11n_block_ack" if profile == "basic" else "wifi_11n_compressed_exchange" + assert f"PROTOCOLTEST {test}: PASS" in run +print("Basic, explicit Compressed, and NED default exchanges passed.") + %contains: stdout PROTOCOLTEST wifi_11n_block_ack: PASS