diff --git a/doc/project/evidence/model/wifi/pr1343-comparison-method.md b/doc/project/evidence/model/wifi/pr1343-comparison-method.md
new file mode 100644
index 00000000000..7c5cabaa2d0
--- /dev/null
+++ b/doc/project/evidence/model/wifi/pr1343-comparison-method.md
@@ -0,0 +1,59 @@
+# Parent-to-commit protocol comparisons
+
+> **Kind:** procedure · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [test rules](../../../rule/testing.md), [protocol authoring](../../../../../tests/protocol/lib/AUTHORING.md)
+
+Each comparison record checks one implementation commit against its first parent. The after revision is the commit that adds the selected protocol fixture. The before revision is that commit's parent. The same fixture inputs and successful assertions apply to both revisions.
+
+The parent lacks the new fixture. The comparison therefore copies the fixture and its listed test components into the parent worktree. It copies no production file. A parent failure must reach the intended protocol assertion. A compilation failure, initialization error, or unavailable observation point does not establish the stated repair.
+
+## Resolve the two revisions
+
+Each record names its fixture and implementation subject. Use the fixture path to locate the fix in the current history:
+
+```sh
+fixture_path=tests/protocol/wifi/common/WifiBlockAckDuplicateAddba.test
+fix_commit="$(git log --diff-filter=A --format=%H -- "$fixture_path")"
+before_commit="$(git rev-parse "$fix_commit^1")"
+```
+
+Replace `fixture_path` with the path from the selected record. The current series adds each fixture once. The lifecycle commit adds both the BAR and inactivity fixtures; either path selects that same commit.
+
+## Prepare identical test inputs
+
+1. Create separate worktrees at `before_commit` and `fix_commit`.
+2. Copy the record's listed fixture inputs from `fix_commit` into the parent worktree.
+3. Preserve the parent production source and its existing test framework.
+4. Source `.envrc` from each worktree root.
+5. Run `make makefiles` in each worktree.
+6. Run `make MODE=debug -j12` in each worktree.
+7. Run the record's filtered protocol command in each worktree.
+
+The after worktree supplies the listed paths without another copy. Original topology, PHY configuration, and protocol framework files remain part of each revision. The before and after builds must match their respective production inputs. The protocol runner builds test executables; it does not replace the INET library build.
+
+## Read the result
+
+Each fixture retains its ordinary PASS expectation. A failed protocol assertion on the parent remains FAIL. A control can pass while another run in the same file fails. The report therefore separates test files from simulation runs and records each control.
+
+All recorded protocol runs use debug mode, configuration `General`, and `seed-set = 0`. Each fixture supplies its run selection and observation windows. The fixtures use the INET Basic compatibility profile. Their guides explain the stimulus and standards limits.
+
+## Identify the executed inputs
+
+Each record pins the before and after source revisions used by the recorded executions. Those hashes identify the actual runs. A later rewrite of documentation can change commit IDs. The rewrite must preserve both compared production inputs and all fixture inputs before it reuses the recorded result.
+
+The production tree hashes identify the tracked `src/` contents independently of commit metadata. Check the current pair with this command:
+
+```sh
+git rev-parse "$before_commit:src" "$fix_commit:src"
+```
+
+Both values must equal the record's production tree hashes. Fixture identity requires a separate check of every listed input. A source tree match alone does not establish fixture identity or execute a test.
+
+The four pairs were executed during the protocol fold. Local evidence resides under `/tmp/pr1343-fold-20261020/`. The parent output directories contain the failed assertions and controls that pass. The after logs contain the successful fixture verdicts at the selected fix. `verification.json` maps those logs to the executed revisions. These local artifacts are outside Git.
+
+This record revision reuses those executions. It does not claim another simulation campaign. Per-commit source and fixture comparisons establish that the documentation rewrite preserves the recorded inputs. The published fixtures and commands permit another execution.
+
+## Standards corpus
+
+The guides preserve the standard references for their own scenarios. The original retrieval used a rebuilt corpus. Its lint output reports 14 heading ambiguities elsewhere in the document, plus other extraction diagnostics. The cited entries resolved to distinct canonical nodes. Table 9-36 also received direct PDF inspection to confirm its reserved value and table alignment.
+
+The checked outgoing references for 11.5.4, 10.3.2.14.3, 11.3.5.5, Table 9-466, and Table 9-467 all resolve. The receiver-cache table and reassociation cross-reference resolve. These checks do not establish a clean verdict for the complete corpus. Local retrieval records and lint output reside under `audit/pr1343-compliant-scenarios-20261020/` outside Git.
diff --git a/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement-comparison.md b/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement-comparison.md
new file mode 100644
index 00000000000..c12bd161a99
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement-comparison.md
@@ -0,0 +1,53 @@
+# BAR without an agreement: parent-to-commit comparison
+
+> **Kind:** report · **Status:** snapshot 2026-10-20 · **Seal:** none · **Owns:** — · **Stands on:** [comparison method](../pr1343-comparison-method.md), [test guide](blockack-bar-without-agreement.md)
+
+The orphan-BAR run fails before the lifecycle fix and passes after that fix. The established-agreement control passes on both revisions. This record covers only `WifiBlockAckBarWithoutAgreement.test`. The inactivity fixture has its own [comparison record](blockack-inactivity-comparison.md).
+
+## Compared revisions
+
+The selected commit has the subject `ieee80211: fix: preserve Block Ack setup and teardown boundaries`. It adds [WifiBlockAckBarWithoutAgreement.test](../../../../../../tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test) and [WifiBlockAckInactivity.test](../../../../../../tests/protocol/wifi/common/WifiBlockAckInactivity.test). Either fixture identifies the same fix in the comparison method.
+
+| Role | Executed source revision | Production tree (`src/`) |
+| --- | --- | --- |
+| Before: parent of the lifecycle fix | `6c90fe0b9b85f199d867ab08bd42097b3437530e` | `3d134699aca48ba038d9dec3a8b947c32b754c2d` |
+| After: lifecycle fix | `d58fe2b72590a195fa69f4c128fddcbb2a7269d8` | `d6fb238177e86a61b28a45ab040d5d9c5a3cf3b8` |
+
+These hashes pin the recorded executions. A documentation rewrite can change current commit IDs without a production or fixture change. Both compared input sets retain their executed contents.
+
+## Results
+
+| Run | Before the commit | After the commit |
+| --- | --- | --- |
+| 0: no agreement | FAIL: no required DELBA within 20 ms | PASS: recipient-role DELBA has the correct peer and TID |
+| 1: established agreement | PASS: Basic Block Ack reaches the controlled peer; no DELBA appears | PASS: the same assertions succeed |
+
+One file contains two simulation runs. The parent failure reaches the orphan-BAR deadline assertion. The after revision passes both runs. Ordinary PASS expectations remain unchanged in both revisions.
+
+## BAR recovery
+
+Before this commit, the recipient does not start the required recovery when a valid BAR arrives without an agreement. After this commit, HCF requests recipient-role DELBA without a recipient agreement allocation. The test requires the correct receiver, TID, and Initiator value.
+
+The stimulus sends a crafted Basic BAR through production Tx and the radio. Run 0 selects TID 5 without an agreement. Run 1 establishes TID 6 through real ADDBA before the BAR. Its controlled peer consumes the Basic Block Ack after production Rx validation. It takes ownership before frame deletion. The recipient response paths remain unchanged by the fixture.
+
+## Reproduce and inspect
+
+Copy these four inputs from the fix into its parent worktree:
+
+- `tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test`.
+- `tests/protocol/wifi/BlockAckLifecycleTestSupport.h`.
+- `tests/protocol/wifi/ned/BlockAckLifecycleHcf.ned`.
+- `tests/protocol/wifi/ini/_blockackLifecycle.ini`.
+
+Run this selector on both debug builds:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckBarWithoutAgreement\.test$' --no-concurrent
+```
+
+The parent output is `06-942a7e2780-parent-output/WifiBlockAckBarWithoutAgreement.out`. The after log is `06-942a7e2780-protocol.log`. That after selector also repeats other fixtures. Only the BAR fixture verdict belongs to this comparison.
+
+## Limits
+
+The fixture supplies the absent agreement directly. It does not reproduce earlier agreement teardown and lost DELBA attempts. Its crafted sender bypasses ordinary EDCA channel access. Basic BAR uses a reserved current-standard type value. The guide explains the legitimate recovery condition and these fixture limits.
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
new file mode 100644
index 00000000000..29a02dff021
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-bar-without-agreement.md
@@ -0,0 +1,131 @@
+# BAR without a Block Ack agreement
+
+> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [test source](../../../../../../tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test), [comparison record](blockack-bar-without-agreement-comparison.md)
+
+A recipient without the selected Block Ack agreement must send recipient-role DELBA after it receives the test BAR. A separate control establishes an agreement and requires a Basic Block Ack response. Together, the runs distinguish recovery from normal response behavior.
+
+The test checks INET's Basic compatibility profile. It does not establish support for every Block Ack format or current-standard conformance for the Basic format.
+
+## Can compliant stations reach this situation?
+
+Yes, after a real agreement becomes inconsistent across the two stations because a teardown frame is lost. The stations need not disagree about the rules. They can temporarily have different information about the agreement.
+
+Hypothetical compliant scenario: the recipient's management entity requests teardown of an active agreement. The recipient removes its local agreement and transmits DELBA. Every DELBA attempt fails to reach the originator because of medium loss. The originator retains its local agreement and later transmits a BAR. The recipient must then recover from the absent-agreement condition.
+
+```mermaid
+sequenceDiagram
+ participant O as Compliant originator
+ participant M as Wireless medium
+ participant R as Compliant recipient
+ Note over O,R: Earlier valid agreement exists at both stations
+ R->>R: Local management requests teardown
+ R->>M: DELBA attempts
+ M->>M: DELBA frames do not reach originator
+ Note over O: Local agreement remains active
+ O->>R: BAR under retained local agreement
+ R->>O: Recipient-role DELBA recovery
+```
+
+[IEEE Std 802.11-2024, 11.5.3.2](../../../../../../standards/80211ax-2024.pdf#page=2565) permits management to initiate teardown through DELBA. [11.5.4](../../../../../../standards/80211ax-2024.pdf#page=2566) specifies DELBA when a recipient receives BAR without an active agreement. The hypothetical loss sequence explains how that receive condition can arise without peer misconduct.
+
+The current fixture starts directly from the absent-agreement state. It does not establish the earlier agreement and lose its DELBA. Its result proves the recovery response for the supplied state, not the complete practical sequence above.
+
+There is also a wire-format limit. This fixture sends a Basic BAR with BAR Type 0, which [Table 9-36](../../../../../../standards/80211ax-2024.pdf#page=719) reserves in IEEE 802.11-2024. The controlled sender also bypasses normal channel access. The current BAR stimulus therefore does not satisfy a strict requirement for fully compliant nodes and frames.
+
+For that requirement, use a permitted BAR variant after real setup and real DELBA loss. Retain the recipient-role recovery assertion. The [profile limits](#profile-limits) distinguish this required follow-up from the current compatibility regression.
+
+## Two runs with different initial state
+
+Both runs use real association, production transmission, and the wireless medium. At `1.1 s`, a controlled station sends one crafted Basic BAR to the AP. The request identifies the selected traffic identifier (TID), sequence 0, and fragment 0.
+
+| Run | State before the crafted BAR | Required AP response |
+| --- | --- | --- |
+| 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.
+
+```mermaid
+sequenceDiagram
+ participant S as Controlled station
+ participant A as Production AP
+ alt Run 0 - no agreement for TID 5
+ S->>A: Basic BAR for TID 5
+ A->>A: Detect absent recipient agreement
+ A-->>S: DELBA for TID 5, Initiator false
+ else Run 1 - agreement for TID 6
+ Note over S,A: Application traffic establishes agreement through ADDBA
+ S->>A: Basic BAR for TID 6
+ A-->>S: Basic Block Ack for TID 6
+ S->>S: Validate and consume control response
+ Note over A: No DELBA during the control window
+ end
+```
+
+The sequence diagram shows distinct runs, not two BAR exchanges within one run. The AP uses production receive and response paths in both cases.
+
+## Assertions and observation windows
+
+Run 0 first requires the AP to receive a Basic BAR from `sta1` for TID 5. The next assertion requires a transmitted DELBA within 20 ms of that reception. DELBA must identify the station and TID. Its Initiator field must be false because the AP ends its recipient role. A query also requires no recipient agreement when DELBA enters transmission.
+
+Run 1 observes the crafted BAR at station transmission. It requires a Basic Block Ack within 20 ms. The response must identify the station and TID 6. The controlled peer must then receive and validate that response through the radio. A concurrent guard prohibits AP DELBA transmission for 20 ms after the BAR transmission.
+
+The no-agreement run observes DELBA transmission at the AP. It does not separately assert DELBA reception or its ACK. The control observes both response transmission and reception. These are different evidence boundaries.
+
+## Why the sender is a controlled peer
+
+The [shared support](../../../../../../tests/protocol/wifi/BlockAckLifecycleTestSupport.h) submits the crafted BAR to production Tx. Tx supplies transmitter addresses and FCS and retains its own frame copy. The fixture deletes its original after submission. Production MAC, radio, and AP receive processing remain active.
+
+The crafted BAR does not create a normal originator exchange context at the sender. The test MAC therefore consumes the control's Basic Block Ack after production Rx validation. It does not submit that response to an idle HCF that never requested it.
+
+```mermaid
+flowchart LR
+ F["Test constructs BAR"] --> T["Production Tx and radio"]
+ T --> R["Production AP receive path"]
+ R --> H["Production agreement decision"]
+ H --> W["Production AP transmission"]
+ W --> P["Controlled peer radio and Rx"]
+ P --> C["Test consumes Basic Block Ack in control run"]
+```
+
+This boundary makes the sender an explicit test component. It leaves the recipient decision under test in production code.
+
+## Recorded result and failure interpretation
+
+The [comparison record](blockack-bar-without-agreement-comparison.md) compares the lifecycle fix with its parent. The parent sends no required DELBA during the observation window. The same run passes after the fix. The established-agreement control passes on both revisions.
+
+If neither run receives a response, first check association, radio reception, and the stimulus. A failure in the control does not isolate absent-agreement recovery. If only run 0 misses DELBA after confirmed BAR reception, the failure reaches the intended recovery obligation.
+
+## Run and scope
+
+From the INET root, use the normal OMNeT++ and INET environment setup. Run the fixture:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckBarWithoutAgreement\.test$' --no-concurrent
+```
+
+The file runs scenarios 0 and 1. Both `wifi_block_ack_orphan_bar` and `wifi_block_ack_known_bar` must report PASS. The file also rejects a protocol FAIL line.
+
+The tests use seed 0 and disable inactivity expiration. They do not test absent-agreement DATA, malformed BAR, response bitmap accuracy, or another Block Ack format. The agreement query checks state at its observation point; it does not observe every possible temporary allocation.
+
+## Profile limits
+
+The shared configuration selects HT stations and `blockAckVariant = "basic"`. This is INET's historical compatibility profile. [IEEE Std 802.11-2024, Table 9-36](../../../../../../standards/80211ax-2024.pdf#page=719) reserves BAR Type 0. INET's Basic BAR uses that value. A run that ends before a BAR appears does not establish conformance for every encoded field.
+
+The fixtures inspect typed frame objects and selected state transitions. They do not check every encoded field or every channel-access requirement. A frame that crosses the simulated radio is not automatically a complete wire-conformance check.
+
+[HCF](../../../../../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) rejects Compressed Block Ack selection during initialization at this stage. A permitted variant requires further implementation support. A configuration change alone cannot supply that support.
+
+## Standards evidence
+
+These references use IEEE Std 802.11-2024, corpus ID `ieee80211-2024`. Physical pages refer to the local file `standards/80211ax-2024.pdf`. Its filename does not identify an ax amendment.
+
+| Subject | Clause, table, or figure | Physical PDF pages | Canonical node ID |
+| --- | --- | --- | --- |
+| BAR variant encoding | Table 9-36 | 719–720 | `ieee80211-2024:table:9-36` |
+| Final-transfer teardown | 10.25.4 | 2056 | `ieee80211-2024:clause:10.25.4` |
+| Management-initiated teardown | 11.5.3.2 | 2565 | `ieee80211-2024:clause:11.5.3.2` |
+| Inactivity and absent-agreement recovery | 11.5.4 | 2566 | `ieee80211-2024:clause:11.5.4` |
+
+The [shared procedure](../pr1343-comparison-method.md#standards-corpus) records the corpus checks, extraction limits, and local retrieval artifacts.
diff --git a/doc/project/evidence/model/wifi/tests/blockack-dialog-token-comparison.md b/doc/project/evidence/model/wifi/tests/blockack-dialog-token-comparison.md
new file mode 100644
index 00000000000..0e2af6efada
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-dialog-token-comparison.md
@@ -0,0 +1,58 @@
+# Negotiation: parent-to-commit comparison
+
+> **Kind:** report · **Status:** snapshot 2026-10-20 · **Seal:** none · **Owns:** — · **Stands on:** [comparison method](../pr1343-comparison-method.md), [test guide](blockack-dialog-token.md)
+
+Two token checks fail before the negotiation fix and pass after that fix. The matching-token control passes on both revisions. This comparison isolates the negotiation commit; it does not compare the endpoints of the PR.
+
+## Compared revisions
+
+The selected commit has the subject `ieee80211: fix: validate Block Ack negotiation parameters`. It adds [WifiBlockAckDialogToken.test](../../../../../../tests/protocol/wifi/common/WifiBlockAckDialogToken.test). The before revision is that commit's first parent.
+
+| Role | Executed source revision | Production tree (`src/`) |
+| --- | --- | --- |
+| Before: parent of the negotiation fix | `6347242ed74b1edde2cd1d9450c9d204af959255` | `fa0b333c8d2f8ae4974b7fa2a0128d01008fdee8` |
+| After: negotiation fix | `77656fef090e0d92a3e9b8b52e641a8313a60a4a` | `e673f4653cb58bd6cf9c1d0660d56ad6994c8d98` |
+
+These hashes pin the recorded executions. A documentation rewrite can change the current commit IDs. The compared production and fixture inputs remain identical. The comparison method explains how to select the current fix and parent.
+
+## Results
+
+| Run | Before the commit | After the commit |
+| --- | --- | --- |
+| 0: token echo | FAIL: response token 1 replaces request token 37 | PASS: response copies token 37; response ACK completes |
+| 1: altered response token | FAIL: originator accepts token 2 for request token 1 | PASS: originator rejects the altered token; no Block Ack-policy DATA appears during the 5 ms guard |
+| 2: matching-token control | PASS: setup succeeds and Block Ack-policy DATA follows | PASS: the same assertions succeed |
+
+One file contains three simulation runs. The parent fails two protocol assertions and passes the control. The after revision passes all three runs. The same successful assertions and ordinary PASS expectations apply to both revisions.
+
+## Cause and assertion boundary
+
+Before this commit, the recipient response retains its default token. The originator acceptance policy also lacks the required token match. After this commit, the recipient copies the request token and the originator policy checks response identity.
+
+Run 0 sends a crafted request through production Tx. The AP receives it through the radio and builds its response through the production handler. The controlled peer checks the token, status, peer, and TID. It also completes the response ACK transmission.
+
+Runs 1 and 2 start setup through application traffic and production HCF. Run 1 changes only the response token before transmission. The fixture captures the actual request token; it does not assume a fixed value. Run 2 leaves the response unchanged and checks normal acceptance.
+
+## Reproduce and inspect
+
+Copy these six inputs from the fix into its parent worktree:
+
+- `tests/protocol/wifi/common/WifiBlockAckDialogToken.test`.
+- `tests/protocol/wifi/BlockAckLifecycleTestSupport.h`.
+- `tests/protocol/wifi/BlockAckRequestProbe.h`.
+- `tests/protocol/wifi/ned/BlockAckLifecycleHcf.ned`.
+- `tests/protocol/wifi/ned/BlockAckRequestProbe.ned`.
+- `tests/protocol/wifi/ini/_blockackLifecycle.ini`.
+
+Run this selector on both debug builds:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckDialogToken\.test$' --no-concurrent
+```
+
+The recorded parent output is `03-f72df99e76-parent-output/WifiBlockAckDialogToken.out`. The after log is `03-f72df99e76-protocol.log`. The after selector also repeats the earlier duplicate fixture. Only the dialog-token verdict belongs to this comparison.
+
+## Limits
+
+The wrong-token mutation is an artificial robustness check. It does not establish a natural late response between compliant peers. The crafted request bypasses ordinary channel access and a complete originator procedure. The guide explains these limits and the Basic compatibility profile.
diff --git a/doc/project/evidence/model/wifi/tests/blockack-dialog-token.md b/doc/project/evidence/model/wifi/tests/blockack-dialog-token.md
new file mode 100644
index 00000000000..d3dc58c9f63
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-dialog-token.md
@@ -0,0 +1,138 @@
+# ADDBA dialog token
+
+> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [test source](../../../../../../tests/protocol/wifi/common/WifiBlockAckDialogToken.test), [comparison record](blockack-dialog-token-comparison.md)
+
+The ADDBA Response must copy the request's dialog token. The originator must also reject a successful response whose token does not match its request. This fixture checks both directions of that contract. A third run confirms that a correct token permits setup and Block Ack-policy DATA.
+
+The dialog token correlates the setup response with its request. It is separate from the MAC sequence number used for retransmission and duplicate detection. A successful status alone does not identify the response as the result of the current setup attempt.
+
+## Can compliant stations reach these situations?
+
+Runs 0 and 2 check normal setup obligations. A compliant originator can choose token 37, and a compliant recipient must copy that token into its response. [IEEE Std 802.11-2024, Table 9-466](../../../../../../standards/80211ax-2024.pdf#page=1650) permits a chosen nonzero token for ordinary solicited setup. [Table 9-467](../../../../../../standards/80211ax-2024.pdf#page=1651) requires the response to copy that request's token.
+
+Run 1 does not establish a compliant-peer scenario. It changes the response token from the current request's value to another value. A compliant recipient must not generate that response for the same request. An ordinary ACK loss does not explain the token change.
+
+| Run | Relevance when every peer behaves correctly |
+| --- | --- |
+| 0: token echo | The selected token and required echo are normal protocol behavior. The controlled sender does not establish complete sender conformance. |
+| 1: changed response token | Artificial input for a robustness check. Exclude this run from evidence restricted to compliant-peer situations. |
+| 2: unchanged response token | Normal setup control through production management and MAC behavior, subject to the common profile limit. |
+
+A delayed response from another dialog would need a separate scenario that creates both dialogs and explains the response delay. This fixture does not create that sequence. The general dialog-token purpose in [10.28.5](../../../../../../standards/80211ax-2024.pdf#page=2093) does not prove that this mutation represents a reachable late-response case.
+
+Run 0 uses a crafted request and a controlled peer without a normal originator procedure. The peer bypasses normal channel access for that request. This isolates the recipient's obligation. It does not prove complete sender behavior. The shared HT configuration also selects the Basic compatibility profile; see the [profile limits](#profile-limits).
+
+For compliant-peer coverage, use the token-echo obligation and the unchanged-response control. The wrong-token run remains separate robustness evidence. It cannot support the claim that the PR repairs a normal exchange between two compliant peers.
+
+## Three runs and their purpose
+
+| Run | Stimulus | Required result |
+| --- | --- | --- |
+| 0 | Controlled peer sends request token 37 for TID 5 | AP response copies token 37 |
+| 1 | Real setup sends token 1; test changes the response token to 2 | Originator rejects the response |
+| 2 | Real setup sends token 1; response remains unchanged | Originator accepts setup and transmits Block Ack-policy DATA |
+
+The token values 1 and 2 in runs 1 and 2 describe the recorded runs. The fixture captures the actual request token. The fault changes the response to a different nonzero token. The comparison therefore remains about equality with the active request.
+
+## Run 0: the recipient must echo the token
+
+The controlled peer sends a crafted ADDBA Request at `1 s`, after real association. The request uses token 37 and traffic identifier (TID) 5. The production AP receives it through the radio and builds its response through its agreement handler.
+
+The test checks the token, TID, successful status, and AP transmitter address at response reception. It also requires completion of the controlled peer's ACK transmission. The controlled peer uses production Tx and Rx, but it does not maintain a normal originator agreement for this crafted request.
+
+```mermaid
+sequenceDiagram
+ participant C as Controlled peer
+ participant A as Production AP MAC
+ participant H as Recipient agreement handler
+ C->>A: ADDBA Request, token 37, TID 5
+ A->>H: Process actual received request
+ H->>H: Copy token into response
+ H->>A: Successful ADDBA Response, token 37
+ A-->>C: Immediate ACK for request
+ A->>C: Transmit response through radio
+ C->>C: Assert token 37, TID, status, and AP address
+ C-->>A: ACK
+ Note over C: Test requires ACK transmission completion
+```
+
+Token 37 makes the echo check sensitive to an unchanged default token of 1. A test that used token 1 for this run could pass despite that defect.
+
+## Runs 1 and 2: the originator must correlate the response
+
+These runs start setup through real application traffic and production HCF for TID 6. The fixture captures the transmitted request token. In run 1, a test HCF changes the response token before production transmission. The response then crosses the radio with its normal status and TID.
+
+The station processes the response through production HCF. A read-only observation checks whether its originator agreement is active after that processing. Run 1 requires an inactive agreement. A concurrent guard also prohibits Block Ack-policy DATA for TID 6 during the next 5 ms.
+
+```mermaid
+sequenceDiagram
+ participant S as Production station
+ participant F as AP test HCF
+ participant A as AP production transmission
+ S->>F: ADDBA Request with actual token T
+ Note over S,F: Fixture captures T from the transmitted request
+ Note over S,F: Request ACK uses the production MAC
+ alt Run 1 - wrong response token
+ F->>F: Replace response token with another nonzero value
+ F->>A: Submit changed response
+ A->>S: Successful response with token different from T
+ S->>S: Reject agreement activation
+ Note over S: No Block Ack-policy DATA for 5 ms
+ else Run 2 - unchanged response token
+ F->>A: Submit unchanged response
+ A->>S: Successful response with token T
+ S->>S: Activate originator agreement
+ S->>A: Block Ack-policy DATA
+ end
+```
+
+Run 2 requires both an active agreement and a transmitted Block Ack-policy DATA frame. This control proves that the same fixture can establish an agreement when token correlation succeeds. The negative guard in run 1 is bounded; it does not claim silence for all later time.
+
+## Test components and assertion boundaries
+
+The response mutator and observer belong to `DialogTokenHcf`. Its `processMgmtFrame()` override changes the selected field before it calls the production method. Its `processLowerFrame()` override calls production receive processing before it emits the agreement observation. The fixture does not implement token acceptance logic itself.
+
+The controlled peer for run 0 comes from [BlockAckRequestProbe.h](../../../../../../tests/protocol/wifi/BlockAckRequestProbe.h). It consumes the response after Rx validation and sends the ACK through production Tx. Runs 1 and 2 leave that controlled-peer mode inactive, so normal originator HCF processes the response.
+
+The assertions first establish the response's actual token relationship. They then inspect the production result. An absent response or absent mutation signal therefore fails before the test can claim correct rejection.
+
+## Recorded result and failure interpretation
+
+The [comparison record](blockack-dialog-token-comparison.md) compares the negotiation fix with its parent. The parent returns the wrong response token and accepts an altered response token. Both checks pass after the fix. The unchanged-token control passes on both revisions.
+
+An echo failure points to response construction after confirmed request reception. An active agreement after the wrong response points to absent correlation at the originator. A correct-token control failure indicates a broader setup problem and needs separate diagnosis.
+
+## Run and scope
+
+From the INET root, use the normal OMNeT++ and INET environment setup. Run the fixture:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckDialogToken\.test$' --no-concurrent
+```
+
+The file runs scenarios 0 through 2 with seed 0. The programs `wifi_block_ack_token_echo`, `wifi_block_ack_wrong_token`, and `wifi_block_ack_matching_token` must all report PASS.
+
+These tests cover the Basic profile, one peer, and the selected tokens. They do not exhaust token wrap, stale responses from an earlier setup, wrong-peer responses, or wrong-TID responses. The wrong-token guard covers Block Ack-policy DATA for 5 ms. It does not establish every later recovery transition or full standards conformance.
+
+## Profile limits
+
+The shared configuration selects HT stations and `blockAckVariant = "basic"`. This is INET's historical compatibility profile. [IEEE Std 802.11-2024, Table 9-36](../../../../../../standards/80211ax-2024.pdf#page=719) reserves BAR Type 0. INET's Basic BAR uses that value. A run that ends before a BAR appears does not establish conformance for every encoded field.
+
+The fixtures inspect typed frame objects and selected state transitions. They do not check every encoded field or every channel-access requirement. A frame that crosses the simulated radio is not automatically a complete wire-conformance check.
+
+[HCF](../../../../../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) rejects Compressed Block Ack selection during initialization at this stage. A permitted variant requires further implementation support. A configuration change alone cannot supply that support.
+
+## Standards evidence
+
+These references use IEEE Std 802.11-2024, corpus ID `ieee80211-2024`. Physical pages refer to the local file `standards/80211ax-2024.pdf`. Its filename does not identify an ax amendment.
+
+| Subject | Clause, table, or figure | Physical PDF pages | Canonical node ID |
+| --- | --- | --- | --- |
+| BAR variant encoding | Table 9-36 | 719–720 | `ieee80211-2024:table:9-36` |
+| Nonzero request token | Table 9-466 under 9.6.4.2 | 1650–1651 | `ieee80211-2024:table:9-466` |
+| Response token echo | Table 9-467 under 9.6.4.3 | 1651–1652 | `ieee80211-2024:table:9-467` |
+| Immediate acknowledgment | 10.3.2.11 | 1906 | `ieee80211-2024:clause:10.3.2.11` |
+| Dialog token purpose | 10.28.5 | 2093 | `ieee80211-2024:clause:10.28.5` |
+
+The [shared procedure](../pr1343-comparison-method.md#standards-corpus) records the corpus checks, extraction limits, and local retrieval artifacts.
diff --git a/doc/project/evidence/model/wifi/tests/blockack-duplicate-addba-comparison.md b/doc/project/evidence/model/wifi/tests/blockack-duplicate-addba-comparison.md
new file mode 100644
index 00000000000..e57d7dbad0f
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-duplicate-addba-comparison.md
@@ -0,0 +1,57 @@
+# Action dispatch: parent-to-commit comparison
+
+> **Kind:** report · **Status:** snapshot 2026-10-20 · **Seal:** none · **Owns:** — · **Stands on:** [comparison method](../pr1343-comparison-method.md), [test guide](blockack-duplicate-addba.md)
+
+The duplicate-request fixture fails before the Action dispatch fix and passes after that fix. The recipient dispatches both request copies before the fix. After the fix, the recipient dispatches once and completes both request ACK transmissions.
+
+## Compared revisions
+
+The selected commit has the subject `ieee80211: fix: dispatch complete Block Ack Actions once`. It adds [WifiBlockAckDuplicateAddba.test](../../../../../../tests/protocol/wifi/common/WifiBlockAckDuplicateAddba.test). The comparison uses that commit and its first parent, as defined by the [comparison method](../pr1343-comparison-method.md).
+
+| Role | Executed source revision | Production tree (`src/`) |
+| --- | --- | --- |
+| Before: parent of the dispatch fix | `54d538def3e4e3c4277afd71550887b5d03f0ea3` | `2cc249d913c5eb4e3ca8f1f90ff69765292d1ea5` |
+| After: dispatch fix | `6347242ed74b1edde2cd1d9450c9d204af959255` | `fa0b333c8d2f8ae4974b7fa2a0128d01008fdee8` |
+
+These hashes pin the recorded executions. The current history can use different commit IDs after a documentation rewrite. Both compared production inputs and the fixture inputs remain identical to those executions.
+
+## Results
+
+| Observation | Before the commit | After the commit |
+| --- | --- | --- |
+| Original request and retry | Two receptions | Two receptions |
+| Request dispatch | Two dispatches; the duplicate assertion fails | One dispatch |
+| Request ACK submission | Two submissions at the failed assertion | Two submissions |
+| Physical ACK completion | The fixture stops at the earlier dispatch failure | Both selected request ACKs complete |
+| Response and later DATA | The fixture stops before these assertions | Response arrives; Block Ack-policy DATA follows |
+| Fixture verdict | FAIL | PASS |
+
+One file contains one simulation run. The parent failure reaches the duplicate-dispatch assertion. It does not result from a test build or initialization error. The after run completes every assertion.
+
+## Cause and assertion boundary
+
+Before this commit, HCF dispatches the management header after the recipient data service rejects its duplicate. After this commit, HCF dispatches only complete frames that the service accepts. The retry still receives its immediate ACK.
+
+The test MAC suppresses the first request ACK at the originator. The test HCF holds the response until the retry arrives. It releases the response one millisecond later. The observer counts only ACK packets submitted for ADDBA Requests, through their packet tree IDs. It excludes unrelated DATA ACKs.
+
+## Reproduce and inspect
+
+Copy these four inputs from the fix into its parent worktree:
+
+- `tests/protocol/wifi/common/WifiBlockAckDuplicateAddba.test`.
+- `tests/protocol/wifi/BlockAckLifecycleTestSupport.h`.
+- `tests/protocol/wifi/ned/BlockAckLifecycleHcf.ned`.
+- `tests/protocol/wifi/ini/_blockackLifecycle.ini`.
+
+Run this selector on both debug builds:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckDuplicateAddba\.test$' --no-concurrent
+```
+
+The recorded parent output is `02-25fb756da1-parent-output/WifiBlockAckDuplicateAddba.out`. The after log is `02-25fb756da1-protocol.log`. Both paths use the local evidence directory from the comparison method. The after selector contains only this fixture.
+
+## Limits
+
+The fixture supplies ACK loss and a controlled response delay. It checks one fixed seed and one retry order. It does not establish complete channel-access behavior or full standards conformance. The guide separates the legitimate retry condition from these test controls.
diff --git a/doc/project/evidence/model/wifi/tests/blockack-duplicate-addba.md b/doc/project/evidence/model/wifi/tests/blockack-duplicate-addba.md
new file mode 100644
index 00000000000..ccf998e1ea6
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-duplicate-addba.md
@@ -0,0 +1,120 @@
+# Duplicate ADDBA Request
+
+> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [test source](../../../../../../tests/protocol/wifi/common/WifiBlockAckDuplicateAddba.test), [comparison record](blockack-duplicate-addba-comparison.md)
+
+The recipient must acknowledge a retried ADDBA Request without a second dispatch to its agreement handler. This test forces that retry through a real wireless exchange. It checks that duplicate detection controls agreement processing while acknowledgment remains available.
+
+An ACK confirms reception of the request. The ADDBA Response carries the setup result. These two frames have different roles. A lost ACK can therefore cause another request transmission even when the recipient already processes the original request.
+
+## Can compliant stations reach this situation?
+
+Yes. A station can transmit a correct request while interference prevents reception of its ACK. The originator then retries the request. The recipient's duplicate cache can identify that retry because it already received the original. Neither station needs to send an incorrect frame for this condition to occur.
+
+Hypothetical compliant scenario: the AP receives the request and sends its ACK, but another transmission corrupts the ACK at the originator. The AP's response awaits channel access while the originator retries. The AP must suppress repeated request processing. It must still acknowledge the retry. [IEEE Std 802.11-2024, 10.3.2.11](../../../../../../standards/80211ax-2024.pdf#page=1906) defines immediate acknowledgment. [10.3.2.14.3](../../../../../../standards/80211ax-2024.pdf#page=1916) defines duplicate detection and disposal.
+
+The fixture models ACK loss at the receive boundary. Its forced response hold supplies the required event order without a second interferer or a production contention scenario. The test therefore proves the response to that event order. It does not prove that its exact one-millisecond hold follows from normal channel access.
+
+The shared configuration selects HT stations and INET's Basic compatibility profile. This is not proof of a complete compliant IEEE 802.11-2024 Block Ack exchange. The [profile limits](#profile-limits) explain the reserved Basic BAR encoding and the required separation between scenario validity and fixture conformance.
+
+For evidence restricted to compliant peers, this scenario is relevant. A stronger test would produce the ACK loss and response delay through ordinary radio and channel-access behavior, with a compliant Block Ack variant.
+
+## Scenario
+
+The network contains an AP and two stations. Application traffic from `sta1` to `sta2` crosses the AP after association. The selected exchange establishes an uplink Basic agreement between `sta1` and the AP for traffic identifier (TID) 6. The stations use the [shared lifecycle configuration](../../../../../../tests/protocol/wifi/ini/_blockackLifecycle.ini), with seed 0 and inactivity expiration disabled.
+
+The test applies two controls to create a specific failure condition. The station's test MAC discards the first addressed ACK after its ADDBA Request transmission. The AP's test HCF also blocks contention while its actual ADDBA Response remains queued. Without that response hold, an early response could resolve setup before the request retry.
+
+```mermaid
+sequenceDiagram
+ participant S as Station production MAC
+ participant F as Station test MAC
+ participant A as AP production HCF
+ participant Q as AP response queue
+ S->>A: ADDBA Request
+ A->>A: Dispatch original request once
+ A->>Q: Queue actual ADDBA Response
+ Note over A,Q: Test blocks response contention
+ A-->>F: Immediate ACK
+ F->>F: Discard this ACK
+ S->>A: Same request with Retry set
+ A->>A: Detect duplicate without another dispatch
+ A-->>S: Immediate ACK
+ Note over A,Q: Test releases queue 1 ms after retry reception
+ Q->>S: ADDBA Response
+ S-->>A: ACK
+ S->>A: DATA under accepted agreement
+```
+
+The diagram shows the causal sequence. The response queue hold does not suppress immediate ACK transmission.
+
+## What the test proves
+
+The fixture compares the retry with the original request. Both must retain the same transmitter, receiver, TID, dialog token, sequence number, and fragment number. Only the retry has its Retry bit set. The test therefore reaches MAC duplicate detection for the original setup request.
+
+The AP must submit two ACKs but dispatch the request only once. A later check requires both ACK transmissions to complete. The test then releases the response queue, observes response reception at the station, and requires Block Ack-policy DATA under an active originator agreement.
+
+```mermaid
+flowchart TD
+ R["Two receptions of the same request"] --> A["Two ACK submissions" ]
+ R --> D["One agreement dispatch"]
+ A --> T["Two completed ACK transmissions"]
+ D --> Q["Release actual response queue"]
+ T --> Q
+ Q --> S["Receive response and accept setup"]
+ S --> F["Transmit Block Ack-policy DATA"]
+```
+
+Each branch supplies different evidence. An ACK count alone cannot prove duplicate suppression. A dispatch count alone cannot prove that the retry still receives its ACK.
+
+## How the fixture observes production behavior
+
+The test MAC, `DuplicateAddbaMac`, supplies the single ACK loss. The test HCF, `DuplicateAddbaHcf`, supplies the response hold and counts calls to production methods. Its dispatch override calls the production method unchanged. The production recipient data service still performs duplicate detection.
+
+The observer identifies request ACKs by their packet tree IDs. It excludes DATA ACKs from the completion count.
+
+The counters distinguish reception, ACK submission, completed ACK transmission, and agreement dispatch. The fixture emits `addbaReception` after production receive processing. The assertion on the second reception can therefore inspect the completed dispatch decision. The later `requestAcksTransmitted` observation checks physical completion before response release.
+
+## Recorded result and failure interpretation
+
+The [comparison record](blockack-duplicate-addba-comparison.md) compares the dispatch fix with its parent. The parent dispatches both request copies. The fix dispatches once, submits two request ACKs, and completes both transmissions. The complete fixture passes after the fix.
+
+| Failure point | Meaning |
+| --- | --- |
+| No proof of ACK loss | The fixture does not establish its required fault. |
+| No request retry | The required duplicate receive path does not occur. |
+| Two dispatches | Duplicate detection does not prevent repeated agreement processing. |
+| Fewer than two completed ACKs | The recipient does not preserve acknowledgment behavior for both receptions. |
+| No later Block Ack-policy DATA | The exchange does not demonstrate successful setup recovery. |
+
+## Run and scope
+
+From the INET root, use the normal OMNeT++ and INET environment setup. Run the single fixture:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckDuplicateAddba\.test$' --no-concurrent
+```
+
+The file contains one simulation run. Its required result is `PROTOCOLTEST wifi_block_ack_duplicate_addba: PASS`.
+
+This test covers the Basic profile and one fixed fault sequence. It does not test duplicate DELBA, fragmented Actions, arbitrary response loss, or retry exhaustion. Its internal dispatch observation complements the wireless trace. It does not establish full standards conformance or upper-layer delivery of the final DATA.
+
+## Profile limits
+
+The shared configuration selects HT stations and `blockAckVariant = "basic"`. This is INET's historical compatibility profile. [IEEE Std 802.11-2024, Table 9-36](../../../../../../standards/80211ax-2024.pdf#page=719) reserves BAR Type 0. INET's Basic BAR uses that value. A run that ends before a BAR appears does not establish conformance for every encoded field.
+
+The fixtures inspect typed frame objects and selected state transitions. They do not check every encoded field or every channel-access requirement. A frame that crosses the simulated radio is not automatically a complete wire-conformance check.
+
+[HCF](../../../../../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) rejects Compressed Block Ack selection during initialization at this stage. A permitted variant requires further implementation support. A configuration change alone cannot supply that support.
+
+## Standards evidence
+
+These references use IEEE Std 802.11-2024, corpus ID `ieee80211-2024`. Physical pages refer to the local file `standards/80211ax-2024.pdf`. Its filename does not identify an ax amendment.
+
+| Subject | Clause, table, or figure | Physical PDF pages | Canonical node ID |
+| --- | --- | --- | --- |
+| BAR variant encoding | Table 9-36 | 719–720 | `ieee80211-2024:table:9-36` |
+| Immediate acknowledgment | 10.3.2.11 | 1906 | `ieee80211-2024:clause:10.3.2.11` |
+| Duplicate detection | 10.3.2.14.3 | 1916–1917 | `ieee80211-2024:clause:10.3.2.14.3` |
+
+The [shared procedure](../pr1343-comparison-method.md#standards-corpus) records the corpus checks, extraction limits, and local retrieval artifacts.
diff --git a/doc/project/evidence/model/wifi/tests/blockack-inactivity-comparison.md b/doc/project/evidence/model/wifi/tests/blockack-inactivity-comparison.md
new file mode 100644
index 00000000000..e78a47441cd
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-inactivity-comparison.md
@@ -0,0 +1,59 @@
+# Inactivity deadline: parent-to-commit comparison
+
+> **Kind:** report · **Status:** snapshot 2026-10-20 · **Seal:** none · **Owns:** — · **Stands on:** [comparison method](../pr1343-comparison-method.md), [test guide](blockack-inactivity.md)
+
+The finite-inactivity run fails before the lifecycle fix and passes after that fix. The zero-timeout control passes on both revisions. This record covers only `WifiBlockAckInactivity.test`. The BAR fixture has its own [comparison record](blockack-bar-without-agreement-comparison.md).
+
+## Compared revisions
+
+The selected commit has the subject `ieee80211: fix: preserve Block Ack setup and teardown boundaries`. It adds [WifiBlockAckBarWithoutAgreement.test](../../../../../../tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test) and [WifiBlockAckInactivity.test](../../../../../../tests/protocol/wifi/common/WifiBlockAckInactivity.test). Either fixture identifies the same fix in the comparison method.
+
+| Role | Executed source revision | Production tree (`src/`) |
+| --- | --- | --- |
+| Before: parent of the lifecycle fix | `6c90fe0b9b85f199d867ab08bd42097b3437530e` | `3d134699aca48ba038d9dec3a8b947c32b754c2d` |
+| After: lifecycle fix | `d58fe2b72590a195fa69f4c128fddcbb2a7269d8` | `d6fb238177e86a61b28a45ab040d5d9c5a3cf3b8` |
+
+These hashes pin the recorded executions. A documentation rewrite can change current commit IDs without a production or fixture change. Both compared input sets retain their executed contents.
+
+## Results
+
+| Run | Before the commit | After the commit |
+| --- | --- | --- |
+| 0: finite timeout | FAIL: recipient agreement remains at its absolute deadline | PASS: agreement disappears at the deadline; timeout DELBA and its ACK complete |
+| 1: zero timeout | PASS: agreement remains active without DELBA | PASS: the same assertions succeed |
+
+One file contains two simulation runs. The parent failure reaches the exact-deadline assertion. The after revision passes both runs. Ordinary PASS expectations remain unchanged in both revisions.
+
+## Absolute inactivity deadline
+
+Before this commit, HCF treats an absolute expiration time as a relative delay. After this commit, HCF schedules the earliest absolute deadline and removes the expired agreement before timer recalculation.
+
+The request and recipient policy both select 20 ms. This equality avoids a dependency on advisory-timeout negotiation. A read-only observer obtains the stored deadline. It requires the agreement one nanosecond before that deadline and no agreement at or one nanosecond after it.
+
+The production expiry event enters the event queue before the boundary observations. A concurrent assertion requires recipient-role DELBA with reason code 39 for the correct peer and TID. Transmission cannot start before the captured deadline. The controlled peer must receive and acknowledge DELBA.
+
+The zero-timeout control requires an infinite deadline and an active agreement at 1.025 s. It prohibits DELBA until 1.031 s. Neither fixture changes the agreement state or the production timer.
+
+## Reproduce and inspect
+
+Copy these six inputs from the fix into its parent worktree:
+
+- `tests/protocol/wifi/common/WifiBlockAckInactivity.test`.
+- `tests/protocol/wifi/BlockAckLifecycleTestSupport.h`.
+- `tests/protocol/wifi/BlockAckRequestProbe.h`.
+- `tests/protocol/wifi/ned/BlockAckLifecycleHcf.ned`.
+- `tests/protocol/wifi/ned/BlockAckRequestProbe.ned`.
+- `tests/protocol/wifi/ini/_blockackLifecycle.ini`.
+
+Run this selector on both debug builds:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckInactivity\.test$' --no-concurrent
+```
+
+The parent output is `06-942a7e2780-parent-output/WifiBlockAckInactivity.out`. The after log is `06-942a7e2780-protocol.log`. That after selector also repeats other fixtures. Only the inactivity fixture verdict belongs to this comparison.
+
+## Limits
+
+The 20 ms timeout is not an integer number of time units. The controlled request bypasses ordinary channel access and a complete originator procedure. The test covers one recipient agreement without DATA refresh. It does not establish originator expiration, concurrent deadlines, or refresh behavior. The guide explains a compliant timeout value and the Basic profile limits.
diff --git a/doc/project/evidence/model/wifi/tests/blockack-inactivity.md b/doc/project/evidence/model/wifi/tests/blockack-inactivity.md
new file mode 100644
index 00000000000..116b9a1ba89
--- /dev/null
+++ b/doc/project/evidence/model/wifi/tests/blockack-inactivity.md
@@ -0,0 +1,124 @@
+# Absolute Block Ack inactivity deadline
+
+> **Kind:** reference · **Status:** current · **Seal:** none · **Owns:** — · **Stands on:** [test source](../../../../../../tests/protocol/wifi/common/WifiBlockAckInactivity.test), [comparison record](blockack-inactivity-comparison.md)
+
+A finite inactivity deadline must retire the recipient agreement at that absolute simulation time. A zero timeout must leave expiration disabled. This fixture checks both conditions after a real ADDBA exchange. It also requires the finite-timeout recipient to transmit timeout DELBA.
+
+The test starts setup after one second of simulation time. This nonzero start exposes confusion between an absolute deadline and a relative delay. If HCF uses that deadline as another delay at time `1 s`, expiration moves to roughly `2.02 s`.
+
+## Can compliant stations reach this situation?
+
+Yes. A recipient can retain an agreement after a compliant originator ends it if the medium loses the originator's DELBA attempts. The recipient then receives no traffic that refreshes its timer. Its finite inactivity timeout must eventually end the recipient's local agreement.
+
+Hypothetical compliant scenario: the originator completes its final data exchange, ends the agreement, and transmits DELBA. The medium loses all DELBA attempts. The recipient still has an agreement but receives no DATA or BAR for that agreement. Its inactivity timer expires and causes recipient-role DELBA. Both stations follow the protocol; frame loss explains their different local state.
+
+[IEEE Std 802.11-2024, 10.25.4](../../../../../../standards/80211ax-2024.pdf#page=2056) defines teardown after the final exchange. [11.5.4](../../../../../../standards/80211ax-2024.pdf#page=2566) defines timer refresh and timeout DELBA. The heading of 11.5.4 mentions peer failure, but its timeout rule also applies when ordinary frame loss leaves stale local state.
+
+The fixture isolates the timer obligation after ADDBA. It does not reproduce a final data exchange and lost DELBA. Its controlled originator sends no DATA after setup, so this initial condition alone is not proof of a complete compliant originator sequence.
+
+The 20 ms timeout is also a model value, not an exact encoded standard value. [9.4.1.14 and Figure 9-152](../../../../../../standards/80211ax-2024.pdf#page=851) express the field in integer time units of 1.024 ms. Twenty milliseconds is not an integer number of those units. A wire-compliant variant could use 20 time units, or 20.48 ms. Zero remains the standard value that disables expiration.
+
+For strict compliant-peer evidence, use a valid encoded timeout after real setup and a justified loss or inactivity sequence. Apply the same before, at, and after deadline assertions. The [profile limits](#profile-limits) also require a permitted Block Ack variant. The current result proves absolute timer behavior within the Basic compatibility model.
+
+## Scenario and two timeout values
+
+A controlled peer sends an ADDBA Request to the production AP at `1 s`. The selected agreement belongs to that peer and traffic identifier (TID) 5. Both the request and recipient policy specify the same timeout. This equality removes negotiation differences from the condition under test.
+
+| Run | Timeout | Required behavior |
+| --- | --- | --- |
+| 0 | 20 ms | Agreement exists before its deadline and disappears at that deadline. The AP sends timeout DELBA. |
+| 1 | 0 s | Agreement has no finite deadline. It remains active without DELBA during the control window. |
+
+The controlled peer acknowledges the actual ADDBA Response. No DATA refreshes the agreement afterward. The observer starts its state checks at `1.001 s`, after response ACK transmission completes.
+
+```mermaid
+sequenceDiagram
+ participant C as Controlled peer
+ participant A as Production AP
+ participant T as Production inactivity timer
+ participant O as Read-only observer
+ C->>A: ADDBA Request, TID 5, timeout 20 ms
+ A->>A: Create recipient agreement and absolute deadline D
+ A-->>C: Request ACK
+ A->>C: ADDBA Response with timeout 20 ms
+ C-->>A: Response ACK
+ O->>A: Read stored deadline D at 1.001 s
+ O->>A: Require agreement at D minus 1 ns
+ T->>A: Expire agreement at D
+ A->>A: Remove recipient agreement
+ O->>A: Require no agreement at D and D plus 1 ns
+ A->>C: Recipient-role DELBA, reason 39
+ C-->>A: ACK
+```
+
+DELBA transmission can start at the deadline. The diagram separates state checks from the wireless exchange; it does not require transmission to wait for the final observer check.
+
+## Exact boundary checks
+
+The fixture reads the agreement's stored expiration time. It does not supply its own expiration time to HCF. In the recorded runs, both revisions store `1.020082495879 s`. The observer requires an active agreement one nanosecond before that time and no agreement at that time or one nanosecond afterward.
+
+```mermaid
+flowchart LR
+ B["D minus 1 ns
Agreement present"] --> E["D
Production expiry event runs first"]
+ E --> C["D
Observer requires agreement absent"]
+ C --> A["D plus 1 ns
Agreement still absent"]
+```
+
+The exact-time assertion depends on a defined event order. Production schedules its expiry event before the observer schedules its boundary events. At equal simulation time, the expiry event therefore executes first. The observer checks state after that production event.
+
+The observer only reads agreement state. It never changes the agreement deadline, cancels the production timer, or invokes teardown. The test thus checks how the production timer uses the stored absolute time.
+
+## DELBA and the zero-timeout control
+
+A concurrent observation requires at least one transmitted DELBA after the observer reads the deadline. DELBA must identify the controlled peer and TID 5. Its Initiator field must be false, and its reason code must be 39. The frame cannot enter transmission before the captured deadline. The controlled peer must receive and acknowledge it.
+
+The frame observation runs concurrently with the boundary checks because DELBA can enter transmission at the deadline itself. A serial check after the final state observation could miss a correct transmission. The concurrent window lasts 30 ms from the deadline-read observation.
+
+In run 1, the observer requires the stored deadline to equal `SIMTIME_MAX`, which represents no finite expiration. It requires an active agreement at `1.025 s`. A concurrent guard prohibits AP DELBA transmission until `1.031 s`. The control therefore extends beyond the finite run's expected expiration time.
+
+## Test components
+
+The [controlled peer](../../../../../../tests/protocol/wifi/BlockAckRequestProbe.h) uses production Tx and Rx for request, response, and ACK exchange. It supplies the request without a normal originator agreement. The production recipient handler creates the AP agreement. Production HCF owns its timer, retirement, and DELBA submission.
+
+The observer, `BlockAckInactivityObserver`, reads the recipient agreement and emits state observations at the selected times. The support query, `recipientDeadline()`, returns the stored absolute expiration time. These names identify observation roles; neither helper performs expiration.
+
+## Recorded result and failure interpretation
+
+The [comparison record](blockack-inactivity-comparison.md) compares the lifecycle fix with its parent. The parent retains the agreement at its absolute deadline. The fix removes it at the deadline and completes the DELBA assertions. The zero-timeout control passes on both revisions.
+
+An agreement absent before the deadline indicates premature cleanup or a failed setup condition. An agreement present at the deadline isolates late retirement under the selected event order. If DELBA is absent after successful retirement, examine teardown transmission and its observation path.
+
+## Run and scope
+
+From the INET root, use the normal OMNeT++ and INET environment setup. Run the fixture:
+
+```sh
+inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \
+ -f '/WifiBlockAckInactivity\.test$' --no-concurrent
+```
+
+The file runs scenarios 0 and 1 with seed 0. Both `wifi_block_ack_inactivity` and `wifi_block_ack_inactivity_disabled` must report PASS.
+
+The fixture covers one Basic recipient agreement without activity refresh. It does not establish originator expiration, several simultaneous deadlines, DATA refresh, or late timer callbacks. The zero-timeout result applies to its finite observation window. These state and frame assertions do not establish full standards conformance.
+
+## Profile limits
+
+The shared configuration selects HT stations and `blockAckVariant = "basic"`. This is INET's historical compatibility profile. [IEEE Std 802.11-2024, Table 9-36](../../../../../../standards/80211ax-2024.pdf#page=719) reserves BAR Type 0. INET's Basic BAR uses that value. A run that ends before a BAR appears does not establish conformance for every encoded field.
+
+The fixtures inspect typed frame objects and selected state transitions. They do not check every encoded field or every channel-access requirement. A frame that crosses the simulated radio is not automatically a complete wire-conformance check.
+
+[HCF](../../../../../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) rejects Compressed Block Ack selection during initialization at this stage. A permitted variant requires further implementation support. A configuration change alone cannot supply that support.
+
+## Standards evidence
+
+These references use IEEE Std 802.11-2024, corpus ID `ieee80211-2024`. Physical pages refer to the local file `standards/80211ax-2024.pdf`. Its filename does not identify an ax amendment.
+
+| Subject | Clause, table, or figure | Physical PDF pages | Canonical node ID |
+| --- | --- | --- | --- |
+| BAR variant encoding | Table 9-36 | 719–720 | `ieee80211-2024:table:9-36` |
+| Time unit definition | 3.1, time unit (TU) | 278 | `ieee80211-2024:definition:time%20unit%20%28TU%29` |
+| Timeout unit and zero value | Figure 9-152 under 9.4.1.14 | 851 | `ieee80211-2024:figure:9-152` |
+| Final-transfer teardown | 10.25.4 | 2056 | `ieee80211-2024:clause:10.25.4` |
+| Inactivity and absent-agreement recovery | 11.5.4 | 2566 | `ieee80211-2024:clause:11.5.4` |
+
+The [shared procedure](../pr1343-comparison-method.md#standards-corpus) records the corpus checks, extraction limits, and local retrieval artifacts.
diff --git a/doc/project/history/ieee80211-blockack-setup-teardown.md b/doc/project/history/ieee80211-blockack-setup-teardown.md
new file mode 100644
index 00000000000..737ac59842c
--- /dev/null
+++ b/doc/project/history/ieee80211-blockack-setup-teardown.md
@@ -0,0 +1,315 @@
+# Block Ack setup and teardown
+
+> **Kind:** history · **Status:** snapshot 2026-10-21 · **Seal:** none · **Owns:** — · **Stands on:** [migration guide](../../src/migration-guide/development/ieee80211-block-ack-agreement-lifecycle.rst), [BAR comparison](../evidence/model/wifi/tests/blockack-bar-without-agreement-comparison.md), [inactivity comparison](../evidence/model/wifi/tests/blockack-inactivity-comparison.md)
+
+The commit `ieee80211: fix: preserve Block Ack setup and teardown boundaries` makes agreement setup and teardown control which DATA frames can proceed. It prevents sequence allocation during unresolved setup, retires expired agreements before timer recalculation, and prevents obsolete exchanges from restoring retired work. These changes connect agreement state to queue extraction, channel access, retransmission, and resource release.
+
+The comparison is with its direct parent, `acdef1761844378991f3afa089f4f7c897825d26`. This snapshot describes those two revisions. The source links identify the responsible files; their worktree contents can include later changes. In particular, this commit does not connect management association replacement to an explicit peer reset. The later commit `ieee80211: fix: clear old peer state before association replacement` adds that connection.
+
+## The problem before the commit
+
+Before this commit, separate components handle agreement events without one common decision about DATA eligibility. HCF can extract more DATA while setup remains unresolved. Extraction can assign sequence numbers that no longer match the first sequence number proposed for the agreement. Teardown can also leave obsolete Actions or unsent fragments associated with a retired agreement.
+
+The inactivity timer has a separate, confirmed defect. Agreement handlers supply an absolute expiration time, but HCF interprets that value as a delay from the current time. An agreement can therefore remain active at its stored deadline.
+
+After this commit, HCF coordinates transitions between the handlers, queue service, and frame exchanges. The handlers retain their agreement and procedure state. HCF decides when a transition requires DATA deferral, retirement, DELBA, or renewed contention.
+
+| Boundary | Before this commit | After this commit |
+| --- | --- | --- |
+| Setup | Further extraction can advance sequence state. | Already numbered DATA resolves before ADDBA; new extraction waits. |
+| Response wait | The policy timeout calculation is unimplemented. | A separate response deadline starts after the request ACK. |
+| Inactivity | HCF treats an absolute time as a delay. | HCF schedules the earliest absolute deadline across both roles. |
+| Teardown | Agreement removal does not coordinate all dependent work. | HCF detaches the role, invalidates Actions, and cleans its DATA. |
+| Channel access | Nonempty queues can represent deferred work. | EDCA checks whether the work is eligible. |
+| Late callbacks | Old exchanges can still reach ordinary protocol updates. | HCF excludes obsolete Actions and removed DATA from those updates. |
+
+## Component responsibilities
+
+An agreement belongs to one peer, one traffic identifier (TID), and one local role. The originator role controls outgoing DATA. The recipient role controls incoming DATA. A station can hold both roles for the same peer and TID; ordinary teardown removes only the selected role.
+
+HCF is the hybrid coordination function. EDCA is its contention mechanism, with one access function, or EDCAF, for each access category. The diagram shows local calls and state observations. It does not show radio timing.
+
+```mermaid
+flowchart TB
+ H["HCF
Coordinates transitions and cleanup"]
+ O["Originator handler
Agreements, setup procedures, response deadlines"]
+ R["Recipient handler
Agreements and pending teardown"]
+ Q["Originator QoS data service
Eligible extraction and A-MSDU permission"]
+ E["EDCA and in-progress frames
Contention and retransmission eligibility"]
+ D["Recipient QoS data service
Reorder release and duplicate history"]
+ F["Frame-sequence handler
Exchange references and completion"]
+ H <--> O
+ H <--> R
+ H --> Q
+ H --> E
+ H --> D
+ H <--> F
+```
+
+The central implementation is [Hcf.cc](../../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc). The [originator handler](../../../src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc) owns the setup procedure map. The [recipient handler](../../../src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc) owns recipient agreements and the set of pending teardowns.
+
+## Setup preserves the first sequence number
+
+Before this commit, the originator handler can build ADDBA directly after a DATA transmission, from that DATA frame's sequence number plus one. That calculation does not first resolve other DATA that already has sequence numbers. The new setup path instead obtains the next unassigned number from the data service, after already numbered DATA resolves.
+
+After an eligible unicast QoS DATA transmission, HCF records a possible need for setup. If no successor exists, the handler enters `WAITING_FOR_SUCCESSOR`. This state permits ordinary DATA access and has no ADDBA request or response deadline. A later queued successor changes the state to `SETUP_INTENT` before extraction.
+
+During `SETUP_INTENT`, already numbered DATA remains eligible. New DATA for the same peer and TID remains in its queue. HCF starts ADDBA only when queued DATA exists and no already numbered DATA remains. HCF also waits for obsolete Actions that still hold an exchange reference.
+
+The handler builds the request with the next sequence number, without consumption of that number. It records a nonzero dialog token and enters `REQUEST_PENDING`. New DATA remains deferred until setup resolves.
+
+```mermaid
+sequenceDiagram
+ participant Q as Pending DATA queue
+ participant H as Originator HCF
+ participant O as Originator handler
+ participant R as Recipient
+ H->>O: Record eligible DATA transmission
+ O->>O: Wait for a successor if necessary
+ Q-->>H: A successor exists
+ H->>O: Enter setup intent
+ H->>H: Resolve already numbered DATA
+ H->>Q: Keep new DATA queued
+ H->>O: Start setup with next unassigned number N
+ O->>R: ADDBA Request with N and dialog token
+ R-->>H: ACK for the request
+ H->>O: Set response deadline
+ R->>H: Matched successful ADDBA Response
+ H->>O: Activate agreement and finish procedure
+ H->>Q: Permit extraction
+ Q-->>H: First new DATA receives N
+```
+
+For example, suppose sequence 40 still needs resolution when another DATA frame arrives for the same peer and TID. HCF permits the work for 40 to finish, but keeps its successor queued. If the next unassigned number is 41, ADDBA proposes 41. After successful setup, the first extracted DATA receives 41. This example is hypothetical; the sequence values illustrate the boundary.
+
+An ACK for ADDBA Request confirms request reception; it does not activate the originator agreement. A matched successful ADDBA Response activates that agreement after policy validation. Peer address, TID, and dialog token identify the pending request. A matched rejection ends setup without an active agreement; an invalid successful response does not activate it.
+
+HCF also connects A-MSDU permission to extraction. An active agreement permits A-MSDU only when both negotiated flags permit it. The ACK policy rejects an already formed A-MSDU that violates this permission. The preceding negotiation commit supplies the flag state; this commit activates its use.
+
+## Uncertain setup requires DELBA recovery
+
+The originator cannot infer recipient state from a lost response. The recipient can accept ADDBA even when the originator receives neither the request ACK nor the response. After a request attempt, setup failure therefore requires teardown before surviving DATA resumes.
+
+HCF records whether the transmitter accepts an ADDBA request attempt. A failed or discarded request without an attempt permits local cancellation. A failed attempted request starts DELBA. A missing response after the response deadline also starts DELBA. DELBA is the Action frame that ends an agreement.
+
+```mermaid
+stateDiagram-v2
+ state "No active agreement" as None
+ state "Wait for successor" as Wait
+ state "Setup intent" as Intent
+ state "Request pending" as Request
+ state "Active agreement" as Active
+ state "DELBA pending" as Teardown
+ state "Recovery needed" as Recovery
+ [*] --> None
+ None --> Wait: Eligible DATA transmits
+ Wait --> Intent: A queued successor exists
+ Intent --> Request: Numbered DATA resolves
+ Intent --> None: No queued DATA remains
+ Request --> Active: Matched accepted response
+ Request --> None: Matched rejection
+ Request --> None: Cancellation before an attempt
+ Request --> Teardown: Attempted request fails or response expires
+ Active --> Teardown: Local retirement requires DELBA
+ Teardown --> None: Originator DELBA receives ACK
+ Teardown --> Recovery: Originator DELBA fails
+ Recovery --> Teardown: Later DATA arrives
+```
+
+The diagram combines handler phases with agreement presence. `Active agreement` and `No active agreement` are explanatory states, not members of `ProcedureState`. Received DELBA can also retire an agreement; the next section explains its direction.
+
+This INET recovery policy uses the DELBA ACK as confirmation before normal access resumes. If originator DELBA fails or the queue discards it, HCF discards the current deferred batch. The handler retains `RECOVERY_NEEDED`. Later DATA triggers one new DELBA exchange; a periodic recovery timer does not run.
+
+For example, a recipient accepts ADDBA but its response is lost. The originator's response deadline starts teardown, while DATA remains deferred. If DELBA also fails, that DATA batch is discarded. A later batch triggers teardown recovery before another setup attempt. Failed recipient-role DELBA instead clears its pending procedure without disposal of outgoing DATA for the opposite role.
+
+The [handler contract](../../../src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h) defines these procedure phases. HCF's `updateAgreementProcedures()` advances them when queue or exchange state changes. Its `beginTeardown()` records teardown, retires the selected role, and queues DELBA.
+
+## Three time limits have different purposes
+
+The response deadline bounds the wait for ADDBA Response after the request ACK. Inactivity bounds an established agreement without qualifying activity. DATA lifetime bounds how long a DATA frame can remain, even while setup or recovery defers it. These limits do not replace one another.
+
+| Time limit | Responsible state | Trigger and result |
+| --- | --- | --- |
+| ADDBA response | Originator procedure | Request ACK sets the deadline; expiry starts originator teardown. |
+| Agreement inactivity | Each agreement role | HCF retires expired agreements and queues timeout DELBA. |
+| DATA lifetime | Existing lifetime handler | Expiry discards DATA and can change the setup procedure. |
+
+The originator policy adds `addbaFailureTimeout`, with a default of 100 ms. It must be positive and finite. Both agreement policies reject a nonpositive maximum buffer size or a negative inactivity timeout. A zero inactivity timeout disables inactivity expiration.
+
+HCF derives one inactivity timer from the earliest absolute deadline across both roles. Agreement handlers request recalculation through `scheduleInactivityTimer()` without a deadline argument. HCF cancels the previous event before it schedules the new absolute time. If no finite deadline exists, HCF leaves the timer unscheduled.
+
+```mermaid
+sequenceDiagram
+ participant A as Agreement handlers
+ participant H as HCF
+ participant T as Inactivity timer
+ participant C as Retirement cleanup
+ A->>H: Request timer recalculation
+ H->>A: Read earliest deadline from each role
+ H->>T: Schedule at the earliest absolute time
+ T->>H: Deadline occurs
+ H->>A: Collect agreements with deadline at or before now
+ H->>A: Record teardown and detach expired agreement
+ H->>C: Invalidate Actions and retire dependent DATA
+ H->>A: Recalculate deadlines after detachment
+ H->>H: Queue timeout DELBA
+```
+
+For a hypothetical agreement created at 1 ms with a 4 ms inactivity timeout, the stored deadline is 5 ms. The parent schedules a 5 ms delay from 1 ms, which places the event at 6 ms. This commit schedules the event at 5 ms. Retirement removes the expired agreement before recalculation, so its deadline cannot cause an immediate expiry loop.
+
+The recipient handler also recalculates expiration after matching Block Ack-policy DATA, including duplicate DATA. The recorded inactivity protocol comparison does not exercise that refresh path. The [inactivity guide](../evidence/model/wifi/tests/blockack-inactivity.md) explains the exact boundary observations and their limits.
+
+## Teardown removes the selected role before notification
+
+Before this commit, received DELBA paths can identify the peer from the receiver address. For a received frame, that address identifies the local station. HCF now uses the transmitter address to identify the peer. The received Initiator field selects the opposite local role.
+
+If the peer sends DELBA as originator, the local station retires its recipient agreement. If the peer sends DELBA as recipient, the local station retires its originator agreement. This action preserves the opposite local role for the same peer and TID.
+
+```mermaid
+flowchart TB
+ E["Retire one peer, TID, and local role"] --> D["Detach agreement from handler lookup"]
+ D --> A["Invalidate Actions for that role"]
+ A --> R{"Which local role?"}
+ R -->|Originator| O["Discard transmitted DATA and unsent siblings
Preserve later intact DATA"]
+ R -->|Recipient| B["Detach reorder buffer
Release complete units and discard incomplete units"]
+ O --> N["Emit deletion notification for an established agreement"]
+ B --> N
+ N --> X["Delete detached agreement"]
+ X --> T["Recalculate inactivity timer"]
+```
+
+Deletion listeners observe an agreement that no longer exists in handler lookup. Cleanup occurs before the synchronous notification. The agreement object remains valid during that notification and is deleted afterward. An unaccepted originator setup does not emit an established-agreement deletion notification.
+
+### Originator disposal follows allocation order
+
+The originator discards transmitted, unacknowledged DATA for the retired agreement. It also discards unsent fragments through the agreement's highest transmitted allocation index. An allocation index records local DATA allocation order before fragmentation. All fragments of that DATA retain the same index, even after ACK processing removes an earlier fragment.
+
+The preceding commit introduces this metadata and the highest-index record. This commit uses that record during retirement. Wire sequence numbers alone cannot establish the boundary across large gaps or wraparound. Retransmission of older DATA cannot move the highest allocation index backward.
+
+```mermaid
+flowchart LR
+ F["Allocation K
Fragment 0 already acknowledged"] --> U["Allocation K
Unsent fragment 1"]
+ U --> L["Allocation K plus 1
Later intact DATA"]
+ B["Agreement retains highest transmitted index K"] --> U
+ U --> D["Discard at teardown"]
+ L --> P["Preserve at ordinary teardown"]
+```
+
+For example, fragment 0 with wire sequence 4095 transmits and receives its ACK. Its unsent siblings still belong to that transmitted allocation. Teardown discards those siblings, but preserves a later intact DATA frame with wire sequence 0. The retained local index supplies the order that the wrapped sequence values cannot supply alone.
+
+HCF also checks disposal of the negotiated first DATA. If that DATA never transmits, a successor cannot silently replace its negotiated sequence boundary. Internal-collision disposal, lifetime expiry, and RTS failure can trigger retirement through `firstDataDiscarded()`. HCF queues DELBA while the successor remains deferred.
+
+### Recipient release preserves complete units and duplicate history
+
+The recipient detaches the reorder buffer and visits its sequence positions in cyclic order. It releases complete units even across a gap, then discards incomplete units. The recipient service defragments released units and deaggregates A-MSDU where necessary before HCF sends them upward.
+
+For example, unit 100 is absent while units 101 and 102 are complete. Ordinary teardown releases 101 and 102 before resource release. This release across a gap is INET policy. It does not reconstruct absent unit 100 or complete a partial unit.
+
+The recipient agreement records the highest received sequence and fragment. Retirement transfers that observation to ordinary duplicate removal. A subsequent retry therefore does not lose all duplicate history merely because the Block Ack agreement ended.
+
+The responsible implementations are [RecipientQosMacDataService](../../../src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc) and [BlockAckReordering](../../../src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc). Peer cleanup helpers also enter in this commit. Their presence does not establish the later management association reset connection.
+
+## Action results and exchange references remain separate
+
+HCF records locally generated ADDBA requests, ADDBA responses, and DELBA frames by Packet identity. The record identifies the peer, TID, role, Action kind, and transmission attempt state. ACK reception, queue disposal, and terminal transmission failure can therefore reach the corresponding procedure result.
+
+The common result method, `actionTerminated()`, removes the ownership record before it changes the procedure. A later callback without that record cannot repeat the same owned Action result. Intentional invalidation instead removes the Action from ownership and marks referenced work obsolete. HCF removes queued obsolete Actions and clears their ACK and applicable retry state.
+
+An exchange can still hold references after logical retirement. HCF leaves those frame objects alive until the frame-sequence handler releases its context. Physical completion can still emit transmission or reception observations. HCF's callback guards prevent those observations from restoring removed DATA or changing an agreement through an obsolete Action.
+
+```mermaid
+sequenceDiagram
+ participant F as Frame-sequence handler
+ participant H as HCF
+ participant T as Transmitter and radio
+ participant I as In-progress frames
+ H->>T: Submit RTS for DATA A
+ H->>H: Agreement retirement removes DATA A
+ H->>I: Mark A dropped and retain referenced storage
+ T-->>F: CTS for the existing exchange
+ F->>H: Request transmission of protected DATA A
+ H-->>F: Refuse because A is no longer in progress
+ F->>H: Complete the exchange
+ F->>F: Delete exchange context
+ F->>I: Clear deferred dropped frames
+```
+
+The diagram shows logical order and omits SIFS and ACK timing. The late CTS does not authorize another DATA frame as a substitute. The transmit path checks membership and eligibility again before submission. Management sequences also retain the original RTS-protected frame across CTS instead of selecting a new queue head.
+
+Received Block Ack Actions need a related rule during a response wait. HCF first submits the received frame to recipient acceptance and duplicate checks. It applies accepted agreement changes before it selects the interrupted exchange's outcome. If the Action resolves the old work, HCF suppresses obsolete failure feedback; an unrelated Action retains ordinary failure feedback.
+
+For example, an ADDBA Response can arrive during the request's final ACK wait after the request ACK is lost. Successful response acceptance invalidates the request before interruption cleanup. A stale request-failure callback must not tear down that newly accepted agreement.
+
+At this commit, HCF selects the Boolean argument to `interruptFrameSequence(notifyFailure)`. The [frame-sequence handler](../../../src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc) performs the selected finish or abort path. The later commit `ieee80211: refactor: let frame sequences own interruption outcomes` moves the outcome decision into the handler. That ownership change is outside this snapshot.
+
+## Deferred DATA does not block eligible traffic
+
+HCF uses the same eligibility conditions for pending extraction, in-progress retransmission, and contention. An obsolete Action is ineligible. During setup intent, already numbered DATA can proceed, while new DATA for that peer and TID waits. During request wait and recovery, affected DATA remains deferred.
+
+```mermaid
+flowchart LR
+ subgraph Q["Pending queue in service order"]
+ A["Peer A, TID 2
Deferred"]
+ B["Peer B, TID 5
Eligible"]
+ C["Peer A, TID 2
Deferred"]
+ A --- B --- C
+ end
+ B --> X["Filtered pull transfers B
with ordinary queue effects"]
+ A -.-> K["Deferred DATA retains queue position
and its original lifetime"]
+ C -.-> K
+```
+
+The selected extraction mechanism enters in an earlier commit. This commit connects HCF's eligibility callbacks to that mechanism. The service uses filtered queue queries and pulls, so eligible DATA can pass a deferred head without removal of the deferred DATA. A nonempty queue alone does not prove that an EDCAF has eligible work.
+
+The [collision controller](../../../src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc) now asks the EDCAF for eligibility. Deferred work cannot cause an internal collision against eligible work in another access category. Existing lifetime checks still apply; deferral does not restart the DATA lifetime.
+
+## Traffic without an agreement triggers recipient recovery
+
+A valid Block Ack-policy DATA frame or Basic BAR can arrive after the recipient loses its agreement. Before this commit, the orphan-BAR path does not initiate the required recovery. HCF now starts recipient-role DELBA without allocation of a replacement recipient agreement or reorder buffer.
+
+HCF checks the supported fields before this recovery path. A Basic BAR must have a valid peer, TID, and sequence number, with supported fragment, bitmap, and Multi-TID fields. Malformed or unsupported inputs do not enter this recovery path.
+
+For a valid DATA frame without an agreement, HCF queues recipient-role DELBA and discards the DATA without upper delivery. For a valid BAR without an agreement, HCF queues DELBA without a normal Block Ack response. An existing pending recipient teardown suppresses another teardown for the same peer and TID.
+
+The [BAR guide](../evidence/model/wifi/tests/blockack-bar-without-agreement.md) describes the absent-agreement scenario and an established-agreement control. This Basic compatibility fixture does not establish support for current-standard Compressed BAR exchanges.
+
+## Configuration and extension effects
+
+Built-in Basic remains the default, including its historical ad hoc and non-HT compatibility. HCF still rejects Compressed Block Ack selection during initialization. The profile checks and MIB notification path prepare later Compressed behavior; they do not enable Compressed exchanges here. Basic skips the new capability-change retirement path.
+
+Custom components must follow the contracts in the [migration guide](../../src/migration-guide/development/ieee80211-block-ack-agreement-lifecycle.rst). The principal effects at this commit are:
+
+1. Agreement handlers expose procedure observations and detach operations so HCF can coordinate retirement without premature deletion.
+2. Inactivity callbacks request recalculation without a deadline argument; HCF derives the earliest absolute time across both roles.
+3. Recipient services support directional retirement, complete-unit release, and peer cleanup through their contract.
+4. Custom frame-sequence handlers support interruption with the caller-selected failure outcome and release contexts before deferred frame deletion.
+5. Custom queue and data-service implementations preserve filtered pull effects, lifetime metadata, and the allocation metadata introduced by earlier commits.
+
+The commit adds no new wire format. It does not implement A-MPDU, delayed Block Ack, Multi-TID exchanges, final-transfer arbitration, or MAC stop/start recovery. It also does not add the later C++ MAC reset interface or its management callers.
+
+## Tests and recorded evidence
+
+The commit adds six module fixtures and extends two existing fixtures. The new fixtures exercise Action disposal, DATA deferral, deferred collision behavior, fragment retirement, lifecycle transitions, and response wait. The [module test directory](../../../tests/module) contains their executable assertions. These fixtures cover selected mechanisms; their presence alone does not prove every production path.
+
+Two new protocol fixtures exercise production reception and recovery paths. Their comparison records report these results:
+
+| Scenario | Recorded parent result | Recorded changed result |
+| --- | --- | --- |
+| Valid BAR without an agreement | Required DELBA does not arrive within 20 ms. | Recipient-role DELBA identifies the correct peer and TID. |
+| BAR with an established agreement | Basic Block Ack arrives; DELBA does not appear. | The same control assertions pass. |
+| Finite recipient inactivity | Agreement remains at its stored absolute deadline. | Agreement disappears at the deadline; timeout DELBA and its ACK complete. |
+| Zero inactivity timeout | Agreement remains active without DELBA. | The same control assertions pass. |
+
+The [BAR comparison](../evidence/model/wifi/tests/blockack-bar-without-agreement-comparison.md) and [inactivity comparison](../evidence/model/wifi/tests/blockack-inactivity-comparison.md) identify the executed revisions. Those revisions precede a documentation rewrite. Their recorded production tree hashes match this commit and its parent: `d6fb238177e86a61b28a45ab040d5d9c5a3cf3b8` and `3d134699aca48ba038d9dec3a8b947c32b754c2d`, respectively.
+
+These are recorded comparisons, not new executions for this explanation. The inactivity fixture covers one recipient agreement without DATA refresh. It does not establish originator expiration, concurrent deadlines, or refresh behavior. Its 20 ms timeout is not an integer number of IEEE 802.11 time units. The crafted BAR sender bypasses ordinary EDCA channel access, and Basic BAR uses a reserved current-standard type value.
+
+Seven functional fingerprint rows change with this commit. They cover ad hoc `MacQos` run 1, infrastructure `MacQosWithBlockAck`, three Block Ack showcases, `HCFfragblockack`, and TXOP `General`. The commit attributes these changes to numbered DATA resolution before ADDBA and setup only when a successor exists. These boundaries change transmitted frames and event order. The fingerprint files retain their graphical expectations.
+
+The [comparison procedure](../evidence/model/wifi/pr1343-comparison-method.md) supplies reproduction instructions. The commit that introduces this document identifies the historical boundary. This lookup remains valid when a documentation amendment changes the commit ID. From the repository root, use:
+
+```sh
+lifecycle_commit="$(git log --diff-filter=A --format=%H -1 -- doc/project/history/ieee80211-blockack-setup-teardown.md)"
+git show "$lifecycle_commit":src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc
+git diff "$lifecycle_commit^" "$lifecycle_commit" -- src/inet/linklayer/ieee80211
+```
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
new file mode 100644
index 00000000000..0212ffe4f7e
--- /dev/null
+++ b/doc/src/migration-guide/development/ieee80211-block-ack-agreement-lifecycle.rst
@@ -0,0 +1,38 @@
+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.
+
+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.
+
+The originator starts ``mac.hcf.originatorBlockAckAgreementPolicy.addbaFailureTimeout`` after the request receives its ACK. Its default is 100 ms. The value must be positive and finite.
+
+If the response does not arrive, HCF sends DELBA before it releases surviving unsent DATA. DATA lifetime separately limits retained DATA. Failed DELBA disposes of the current deferred batch. Later DATA starts one fresh recovery exchange.
+
+At ordinary teardown, the originator discards every transmitted, unacknowledged MPDU for the selected agreement. This includes DATA whose lifetime remains valid when inactivity expires. It also discards unsent fragments through the highest transmitted allocation index, even if an earlier fragment received an ACK. This local index records allocation order before fragmentation. Later unsent DATA retains sequence continuity.
+
+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.
+
+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.
+
+``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. Ordinary directional teardown preserves the opposite role.
+
+Custom QoS recipient services must return accepted, complete Actions from ``managementFrameReceived()``. HCF owns their agreement dispatch after duplicate checks. Services must implement directional retirement and peer cleanup through the recipient-service contract. Custom agreement policies must implement ``isProfileAllowed()`` with the supplied typed profile and usable capability inputs. Basic admission must preserve its historical non-HT compatibility.
+
+Custom agreement handlers must implement the new procedure and retirement operations in their handler contracts. The originator handler owns setup intent, request identity, response deadlines, teardown, and recovery state. Its ``noteEligibleDataTransmission(peer, tid)`` operation records ``WAITING_FOR_SUCCESSOR`` after an eligible DATA transmission. This phase permits ordinary DATA access and has no request or deadline. A later queued successor promotes the phase to setup intent before extraction. Peer reset clears the phase.
+
+The recipient handler owns its active agreements and pending teardown. Detachment removes an agreement from lookup and transfers it to the caller. The caller retains the object through its deletion callback. Const observations expose current state without a second agreement store.
+
+Custom implementations of ``IBlockAckAgreementHandlerCallback`` must implement ``scheduleInactivityTimer()`` without a deadline argument. Agreement handlers request recomputation after their state changes. HCF queries both roles and schedules their earliest absolute deadline. It cancels the timer when neither role has a finite deadline. Custom callers must remove the old deadline argument.
+
+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 *``.
+
+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.
+
+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.
+
+External frame-sequence handlers must implement ``interruptFrameSequence(notifyFailure)``. A false argument ends an exchange that an accepted Action already resolves, without obsolete 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/whatsnew/compatible/queueing-packet-filters-can-retain-local-state.txt b/doc/whatsnew/compatible/queueing-packet-filters-can-retain-local-state.txt
new file mode 100644
index 00000000000..061bf1d22e0
--- /dev/null
+++ b/doc/whatsnew/compatible/queueing-packet-filters-can-retain-local-state.txt
@@ -0,0 +1,3 @@
+Packet filters can retain local state
+
+CPacketFilterFunction now accepts a callable with captured values. A consumer can supply its current acceptance condition to canPullPacket() and pullPacket(). Existing function pointers remain valid. Filter queries must not change simulation state.
diff --git a/doc/whatsnew/incompatible/ieee80211-block-ack-agreement-lifecycle.txt b/doc/whatsnew/incompatible/ieee80211-block-ack-agreement-lifecycle.txt
new file mode 100644
index 00000000000..8c4928b0cd9
--- /dev/null
+++ b/doc/whatsnew/incompatible/ieee80211-block-ack-agreement-lifecycle.txt
@@ -0,0 +1,15 @@
+IEEE 802.11 Block Ack agreement lifecycle
+
+HCF now dispatches accepted Block Ack Actions once after duplicate checks. Setup correlates the peer, TID, and dialog token. The recipient response determines the accepted buffer size and inactivity timeout. Both negotiated flags control A-MSDU use.
+
+HCF defers new DATA extraction during setup and recovery. The first DATA under an accepted agreement retains the request sequence number. The response timeout starts after the request ACK, with a configurable default of 100 ms. Uncertain setup requires DELBA before surviving unsent DATA can resume. Failed DELBA discards the current deferred batch. Later DATA can start a fresh recovery exchange.
+
+If an internal collision discards the first DATA before transmission, HCF retires the agreement and queues DELBA. The unsent successor remains deferred until recovery resolves. Lifetime expiry and RTS failure use the same retirement mechanism.
+
+Directional teardown preserves the opposite agreement. The originator discards transmitted, unacknowledged DATA, even when its lifetime remains valid at inactivity expiration. It also discards unsent fragments through the highest transmitted allocation index. This local index records allocation order before fragmentation. This prevents transmission of an incomplete unit outside the agreement. Later intact DATA remains eligible, including across sequence wraparound.
+
+Originator retirement now uses local allocation indices to preserve this boundary across large sequence gaps. A distance of exactly 2048 between DATA transmissions no longer lets unsent fragments survive teardown. The wire sequence numbers remain unchanged.
+
+The recipient delivers complete buffered units in cyclic order across receive gaps before resource release. These changes can alter delivery, disposal, frame exchanges, and fingerprints.
+
+Basic remains the default. Compressed selection still stops initialization. Custom agreement handlers and recipient services 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.
diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag.msg
new file mode 100644
index 00000000000..3d586aad6c0
--- /dev/null
+++ b/src/inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag.msg
@@ -0,0 +1,17 @@
+//
+// Copyright (C) 2026 OpenSim Ltd.
+//
+// SPDX-License-Identifier: LGPL-3.0-or-later
+//
+
+import inet.common.INETDefs;
+import inet.common.TagBase;
+
+namespace inet::ieee80211;
+
+// Local allocation order before fragmentation. All fragments retain this index.
+// Unlike the wire sequence number, the index does not wrap at 4096.
+class Ieee80211DataSequenceTag extends TagBase
+{
+ int64_t allocationIndex = -1;
+}
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h
index a516527bc34..8ef73652db3 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h
+++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h
@@ -24,6 +24,9 @@ class INET_API OriginatorBlockAckAgreement : public cObject
SequenceNumberCyclic startingSequenceNumber;
int bufferSize = -1;
bool isAMsduSupported = false;
+ bool responseAMsduSupported = false;
+ int dialogToken = -1;
+ int64_t highestTransmittedDataIndex = -1;
bool isDelayedBlockAckPolicySupported = false;
bool isAddbaResponseReceived = false;
bool isAddbaRequestSent = false;
@@ -49,6 +52,19 @@ class INET_API OriginatorBlockAckAgreement : public cObject
virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; }
virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; }
virtual bool getIsAMsduSupported() const { return isAMsduSupported; }
+ // IEEE Std 802.11-2024, 10.25.2 defines ADDBA negotiation; INET requires both flags before A-MSDU aggregation.
+ bool isAMsduPermitted() const { return isAMsduSupported && responseAMsduSupported; }
+ int getDialogToken() const { return dialogToken; }
+ void setDialogToken(int token) { dialogToken = token; }
+ void setResponseAMsduSupported(bool value) { responseAMsduSupported = value; }
+ bool getFirstDataTransmitted() const { return highestTransmittedDataIndex >= 0; }
+ int64_t getHighestTransmittedDataIndex() const { return highestTransmittedDataIndex; }
+ void recordTransmittedData(int64_t allocationIndex)
+ {
+ ASSERT(allocationIndex >= 0);
+ if (highestTransmittedDataIndex < allocationIndex)
+ highestTransmittedDataIndex = allocationIndex;
+ }
virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; }
virtual MacAddress getReceiverAddr() const { return receiverAddr; }
virtual Tid getTid() const { return tid; }
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc
index 2f11dc365db..fdc8c44dd1e 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc
+++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc
@@ -8,15 +8,108 @@
#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h"
#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h"
+#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h"
namespace inet {
namespace ieee80211 {
+void OriginatorBlockAckAgreementHandler::noteEligibleDataTransmission(MacAddress peer, Tid tid)
+{
+ Procedure procedure;
+ procedure.state = ProcedureState::WAITING_FOR_SUCCESSOR;
+ procedures.emplace(AgreementId(peer, tid), procedure);
+}
+
+void OriginatorBlockAckAgreementHandler::beginSetupIntent(MacAddress peer, Tid tid)
+{
+ auto result = procedures.emplace(AgreementId(peer, tid), Procedure());
+ if (result.first->second.state == ProcedureState::WAITING_FOR_SUCCESSOR)
+ result.first->second.state = ProcedureState::SETUP_INTENT;
+}
+
+void OriginatorBlockAckAgreementHandler::beginTeardown(MacAddress peer, Tid tid)
+{
+ Procedure procedure;
+ procedure.state = ProcedureState::TEARDOWN_PENDING;
+ procedures[{peer, tid}] = procedure;
+}
+
+void OriginatorBlockAckAgreementHandler::failTeardown(MacAddress peer, Tid tid)
+{
+ Procedure procedure;
+ procedure.state = ProcedureState::RECOVERY_NEEDED;
+ procedures[{peer, tid}] = procedure;
+}
+
+void OriginatorBlockAckAgreementHandler::finishProcedure(MacAddress peer, Tid tid)
+{
+ procedures.erase({peer, tid});
+}
+
+void OriginatorBlockAckAgreementHandler::markRequestAttempted(MacAddress peer, Tid tid)
+{
+ auto it = procedures.find({peer, tid});
+ if (it != procedures.end() && it->second.state == ProcedureState::REQUEST_PENDING)
+ it->second.requestAttempted = true;
+}
+
+void OriginatorBlockAckAgreementHandler::acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout)
+{
+ auto it = procedures.find({peer, tid});
+ if (it != procedures.end() && it->second.state == ProcedureState::REQUEST_PENDING)
+ it->second.responseDeadline = simTime() + timeout;
+}
+
void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest)
{
OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0);
auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid());
blockAckAgreements[agreementId] = blockAckAgreement;
+ blockAckAgreement->setDialogToken(addbaRequest->getDialogToken());
+ procedures[agreementId].state = ProcedureState::REQUEST_PENDING;
+}
+
+OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::detachAgreement(MacAddress peer, Tid tid)
+{
+ auto it = blockAckAgreements.find({peer, tid});
+ if (it == blockAckAgreements.end())
+ return nullptr;
+ auto agreement = it->second;
+ blockAckAgreements.erase(it);
+ return agreement;
+}
+
+std::vector OriginatorBlockAckAgreementHandler::detachPeer(MacAddress peer)
+{
+ std::vector removed;
+ for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) {
+ if (it->first.first == peer) {
+ removed.push_back(it->second);
+ it = blockAckAgreements.erase(it);
+ }
+ else
+ ++it;
+ }
+ for (auto it = procedures.begin(); it != procedures.end();) {
+ if (it->first.first == peer)
+ it = procedures.erase(it);
+ else
+ ++it;
+ }
+ return removed;
+}
+
+void OriginatorBlockAckAgreementHandler::startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence,
+ IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu)
+{
+ auto request = buildAddbaRequest(peer, tid, sequence, policy);
+ request->setAMsduSupported(aMsdu && policy->isMsduSupported());
+ // IEEE Std 802.11-2024, Table 9-466: ordinary setup uses a nonzero dialog token.
+ auto& token = dialogTokens[{peer, tid}];
+ token = token % 255 + 1;
+ request->setDialogToken(token);
+ createAgreement(request);
+ callback->processMgmtFrame(new Packet("AddbaReq", request), request);
}
simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime()
@@ -40,15 +133,16 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall
// primitive with the ReasonCode parameter having a value of TIMEOUT.
// The procedure is illustrated in Figure 10-14.
simtime_t now = simTime();
+ std::vector expired;
for (auto id : blockAckAgreements) {
auto agreement = id.second;
- if (agreement->getExpirationTime() == now) {
- MacAddress receiverAddr = id.first.first;
- Tid tid = id.first.second;
- const auto& delba = buildDelba(receiverAddr, tid, 39);
- auto delbaPacket = new Packet("Delba", delba);
- procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes
- }
+ if (agreement->getIsAddbaResponseReceived() && agreement->getExpirationTime() <= now)
+ expired.push_back(id.first);
+ }
+ for (const auto& id : expired) {
+ const auto& delba = buildDelba(id.first, id.second, 39);
+ auto delbaPacket = new Packet("Delba", delba);
+ procedureCallback->processMgmtFrame(delbaPacket, delba);
}
scheduleInactivityTimer(agreementHandlerCallback);
}
@@ -87,9 +181,7 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime);
+ callback->scheduleInactivityTimer();
}
OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid)
@@ -124,12 +216,8 @@ void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originato
void OriginatorBlockAckAgreementHandler::processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback)
{
auto agreement = getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid());
- if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr) {
- auto addbaReq = buildAddbaRequest(dataHeader->getReceiverAddress(), dataHeader->getTid(), dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy);
- createAgreement(addbaReq);
- auto addbaPacket = new Packet("AddbaReq", addbaReq);
- callback->processMgmtFrame(addbaPacket, addbaReq);
- }
+ if (agreement && agreement->getIsAddbaResponseReceived())
+ agreement->recordTransmittedData(packet->getTag()->getAllocationIndex());
}
void OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback)
@@ -149,6 +237,7 @@ void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgree
agreement->setIsAddbaResponseReceived(true);
agreement->setBufferSize(addbaResp->getBufferSize());
agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue());
+ agreement->setResponseAMsduSupported(addbaResp->getAMsduSupported());
agreement->calculateExpirationTime();
}
@@ -157,13 +246,11 @@ void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(const PtrgetReceiverAddress(), addbaReq->getTid());
if (agreement)
agreement->setIsAddbaRequestSent(true);
- else
- throw cRuntimeError("Block Ack Agreement should have already been added");
}
void OriginatorBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba)
{
- terminateAgreement(delba->getReceiverAddress(), delba->getTid());
+ // HCF retires the agreement before it queues DELBA. ACK completes teardown.
}
void OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy)
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h
index 4a449d17da3..af0c99d0cb7 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h
+++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h
@@ -21,6 +21,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg
{
protected:
std::map, OriginatorBlockAckAgreement *> blockAckAgreements;
+ std::map procedures;
+ std::map dialogTokens;
protected:
virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy);
@@ -28,10 +30,22 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg
virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp);
virtual void terminateAgreement(MacAddress originatorAddr, Tid tid);
virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode);
- virtual simtime_t computeEarliestExpirationTime();
virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback);
public:
+ virtual simtime_t computeEarliestExpirationTime() override;
+ const std::map& getAgreements() const override { return blockAckAgreements; }
+ const std::map& getProcedures() const override { return procedures; }
+ virtual void noteEligibleDataTransmission(MacAddress peer, Tid tid) override;
+ virtual void beginSetupIntent(MacAddress peer, Tid tid) override;
+ virtual void beginTeardown(MacAddress peer, Tid tid) override;
+ virtual void failTeardown(MacAddress peer, Tid tid) override;
+ virtual void finishProcedure(MacAddress peer, Tid tid) override;
+ virtual void markRequestAttempted(MacAddress peer, Tid tid) override;
+ virtual void acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) override;
+ OriginatorBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) override;
+ std::vector detachPeer(MacAddress peer) override;
+ void startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) override;
virtual ~OriginatorBlockAckAgreementHandler();
virtual void processTransmittedAddbaReq(const Ptr& addbaReq) override;
virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override;
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc
index 44b4c0874fa..835216df923 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc
+++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc
@@ -9,6 +9,7 @@
#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h"
#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h"
+#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h"
namespace inet {
namespace ieee80211 {
@@ -24,15 +25,23 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage)
aMsduSupported = par("aMsduSupported");
maximumAllowedBufferSize = par("maximumAllowedBufferSize");
blockAckTimeoutValue = par("blockAckTimeoutValue");
- // TODO addbaFailureTimeout = par("addbaFailureTimeout");
+ addbaFailureTimeout = par("addbaFailureTimeout");
+ if (addbaFailureTimeout <= SIMTIME_ZERO || addbaFailureTimeout == SIMTIME_MAX)
+ throw cRuntimeError("addbaFailureTimeout must be positive and finite");
+ if (maximumAllowedBufferSize <= 0 || blockAckTimeoutValue < SIMTIME_ZERO)
+ throw cRuntimeError("Invalid Block Ack buffer size or inactivity timeout");
WATCH(blockAckReqThreshold);
}
}
simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const
{
- // TODO ADDBAFailureTimeout -- 6.3.29.2.2 Semantics of the service primitive
- throw cRuntimeError("Unimplemented");
+ return addbaFailureTimeout;
+}
+
+bool OriginatorBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const
+{
+ return variant == BlockAckVariant::BASIC || (localHt && peerHt);
}
bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const Ptr& header)
@@ -42,8 +51,16 @@ bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const P
bool OriginatorBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement)
{
- ASSERT(agreement);
- return true;
+ // IEEE Std 802.11-2024, 10.25.2 and Table 9-466: match the pending setup and its nonzero dialog token.
+ // Immediate Block Ack and the 64-entry bound are INET profile limits for historical Basic compatibility.
+ return agreement != nullptr && !agreement->getIsAddbaResponseReceived() &&
+ addbaResp->getTransmitterAddress() == agreement->getReceiverAddr() &&
+ addbaResp->getTid() == agreement->getTid() &&
+ addbaResp->getDialogToken() > 0 && addbaResp->getDialogToken() <= 255 &&
+ addbaResp->getDialogToken() == agreement->getDialogToken() &&
+ addbaResp->getStatusCode() == SC_SUCCESSFUL && addbaResp->getBlockAckPolicy() &&
+ addbaResp->getBufferSize() > 0 && addbaResp->getBufferSize() <= 64 &&
+ addbaResp->getBlockAckTimeoutValue() >= SIMTIME_ZERO;
}
bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba)
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h
index b762cc29828..b0b7e9cce24 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h
+++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h
@@ -32,6 +32,7 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi
virtual void initialize(int stage) override;
public:
+ virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override;
virtual bool isAddbaReqNeeded(Packet *packet, const Ptr& header) override;
virtual bool isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) override;
virtual bool isDelbaAccepted(const Ptr& delba) override;
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned
index bbb569b4b20..ab9970f883f 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned
+++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned
@@ -21,8 +21,9 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl
bool delayedAckPolicySupported = default(false);
bool aMsduSupported = default(true);
- int maximumAllowedBufferSize = default(64);
- double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator
+ int maximumAllowedBufferSize = default(64); // The ADDBA Request uses this advisory buffer size; the recipient response determines the accepted allocation.
+ double blockAckTimeoutValue @unit(s) = default(0s); // Zero disables inactivity expiration.
+ double addbaFailureTimeout @unit(s) = default(100ms); // Response wait after request ACK.
@display("i=block/control");
}
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h
index 40a4186fead..6e9634cae6f 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h
+++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h
@@ -23,6 +23,8 @@ class INET_API RecipientBlockAckAgreement : public cObject
simtime_t blockAckTimeoutValue = 0;
bool isAddbaResponseSent = false;
simtime_t expirationTime = -1;
+ SequenceNumberCyclic highestReceivedSequence;
+ int highestReceivedFragment = 0;
public:
RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue);
@@ -33,6 +35,17 @@ 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; }
+ void recordReceived(const Ptr& header) {
+ auto sequence = header->getSequenceNumber();
+ if (!highestReceivedSequence.isSpecified() || highestReceivedSequence < sequence) {
+ highestReceivedSequence = sequence;
+ highestReceivedFragment = header->getFragmentNumber();
+ }
+ else if (highestReceivedSequence == sequence)
+ highestReceivedFragment = std::max(highestReceivedFragment, int(header->getFragmentNumber()));
+ }
+ const SequenceNumberCyclic& getHighestReceivedSequence() const { return highestReceivedSequence; }
+ int getHighestReceivedFragment() const { return highestReceivedFragment; }
virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; }
virtual void addbaResposneSent() { isAddbaResponseSent = true; }
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc
index 6219ac77140..717368a5a46 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc
+++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc
@@ -8,6 +8,7 @@
#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h"
#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h"
+#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h"
namespace inet {
namespace ieee80211 {
@@ -24,9 +25,7 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime()
void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback)
{
- simtime_t earliestExpirationTime = computeEarliestExpirationTime();
- if (earliestExpirationTime != SIMTIME_MAX)
- callback->scheduleInactivityTimer(earliestExpirationTime);
+ callback->scheduleInactivityTimer();
}
// The inactivity timer at a recipient is reset when MPDUs corresponding to the TID for which the Block Ack
@@ -35,12 +34,15 @@ void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreeme
//
void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback)
{
+ // IEEE Std 802.11-2024, 11.5.4: matching Block Ack-policy DATA refreshes inactivity, including a duplicate.
if (qosHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { // TODO + Implicit Block Ack
Tid tid = qosHeader->getTid();
MacAddress originatorAddr = qosHeader->getTransmitterAddress();
auto agreement = getAgreement(tid, originatorAddr);
- if (agreement)
+ if (agreement) {
+ agreement->calculateExpirationTime();
scheduleInactivityTimer(callback);
+ }
}
}
@@ -51,15 +53,16 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb
// primitive with the ReasonCode parameter having a value of TIMEOUT.
// The procedure is illustrated in Figure 10-14.
simtime_t now = simTime();
+ std::vector> expired;
for (auto id : blockAckAgreements) {
auto agreement = id.second;
- if (agreement->getExpirationTime() == now) {
- MacAddress receiverAddr = id.first.first;
- Tid tid = id.first.second;
- const auto& delba = buildDelba(receiverAddr, tid, 39);
- auto delbaPacket = new Packet("Delba", delba);
- procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes
- }
+ if (agreement->getExpirationTime() <= now)
+ expired.push_back(id.first);
+ }
+ for (const auto& id : expired) {
+ const auto& delba = buildDelba(id.first, id.second, 39);
+ auto delbaPacket = new Packet("Delba", delba);
+ procedureCallback->processMgmtFrame(delbaPacket, delba);
}
scheduleInactivityTimer(agreementHandlerCallback);
}
@@ -110,8 +113,10 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR
Tid tid = addbaRequest->getTid();
addbaResponse->setTid(tid);
addbaResponse->setBlockAckPolicy(!addbaRequest->getBlockAckPolicy() && blockAckAgreementPolicy->delayedBlockAckPolicySupported() ? false : true);
- addbaResponse->setBufferSize(addbaRequest->getBufferSize() <= blockAckAgreementPolicy->getMaximumAllowedBufferSize() ? addbaRequest->getBufferSize() : blockAckAgreementPolicy->getMaximumAllowedBufferSize());
- addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? blockAckAgreementPolicy->getBlockAckTimeoutValue() : addbaRequest->getBlockAckTimeoutValue());
+ addbaResponse->setDialogToken(addbaRequest->getDialogToken());
+ // IEEE Std 802.11-2024, 10.25.2: the request allocation and timeout are advisory.
+ addbaResponse->setBufferSize(std::min(64, blockAckAgreementPolicy->getMaximumAllowedBufferSize()));
+ addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue());
addbaResponse->setAMsduSupported(blockAckAgreementPolicy->aMsduSupported());
return addbaResponse;
}
@@ -124,8 +129,6 @@ void RecipientBlockAckAgreementHandler::updateAgreement(const Ptrsecond;
agreement->addbaResposneSent();
}
- else
- throw cRuntimeError("Agreement is not found");
}
void RecipientBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid)
@@ -155,26 +158,62 @@ void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback)
{
EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl;
- if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest)) {
+ auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid());
+ auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy);
+ bool accepted = blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest) &&
+ blockAckAgreements.count(id) == 0 && pendingTeardowns.count(id) == 0;
+ addbaResponse->setStatusCode(accepted ? SC_SUCCESSFUL : SC_UNSPECIFIED);
+ if (accepted) {
EV_DETAIL << "Addba Request has been accepted. Creating a new Block Ack Agreement." << endl;
- auto agreement = addAgreement(addbaRequest);
+ auto parameters = makeShared(*addbaRequest);
+ parameters->setBufferSize(addbaResponse->getBufferSize());
+ parameters->setBlockAckTimeoutValue(addbaResponse->getBlockAckTimeoutValue());
+ auto agreement = addAgreement(parameters);
EV_DETAIL << "Agreement is added with the following parameters: " << *agreement << endl;
EV_DETAIL << "Building Addba Response" << endl;
- auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy);
- auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse);
- callback->processMgmtFrame(addbaResponsePacket, addbaResponse);
}
+ callback->processMgmtFrame(new Packet("AddbaResponse", addbaResponse), addbaResponse);
+}
+
+RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::detachAgreement(MacAddress peer, Tid tid)
+{
+ auto it = blockAckAgreements.find({peer, tid});
+ if (it == blockAckAgreements.end())
+ return nullptr;
+ auto agreement = it->second;
+ blockAckAgreements.erase(it);
+ return agreement;
+}
+
+std::vector RecipientBlockAckAgreementHandler::detachPeer(MacAddress peer)
+{
+ std::vector removed;
+ for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) {
+ if (it->first.first == peer) {
+ removed.push_back(it->second);
+ it = blockAckAgreements.erase(it);
+ }
+ else
+ ++it;
+ }
+ for (auto it = pendingTeardowns.begin(); it != pendingTeardowns.end();) {
+ if (it->first == peer)
+ it = pendingTeardowns.erase(it);
+ else
+ ++it;
+ }
+ return removed;
}
void RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba)
{
- terminateAgreement(delba->getReceiverAddress(), delba->getTid());
+ // HCF retains teardown state until the Action reaches a terminal result.
}
void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy)
{
if (blockAckAgreementPolicy->isDelbaAccepted(delba))
- terminateAgreement(delba->getReceiverAddress(), delba->getTid());
+ terminateAgreement(delba->getTransmitterAddress(), delba->getTid());
}
RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler()
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h
index 1e65a1557ef..ac93c8366e6 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h
+++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h
@@ -26,6 +26,7 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre
{
protected:
std::map, RecipientBlockAckAgreement *> blockAckAgreements;
+ std::set> pendingTeardowns;
protected:
virtual void terminateAgreement(MacAddress originatorAddr, Tid tid);
@@ -33,10 +34,16 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre
virtual void updateAgreement(const Ptr& addbaResponse);
virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy);
virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode);
- virtual simtime_t computeEarliestExpirationTime();
virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback);
public:
+ virtual simtime_t computeEarliestExpirationTime() override;
+ const std::map, RecipientBlockAckAgreement *>& getAgreements() const override { return blockAckAgreements; }
+ bool isTeardownPending(MacAddress peer, Tid tid) const override { return pendingTeardowns.count({peer, tid}) != 0; }
+ void beginTeardown(MacAddress peer, Tid tid) override { pendingTeardowns.insert({peer, tid}); }
+ void finishTeardown(MacAddress peer, Tid tid) override { pendingTeardowns.erase({peer, tid}); }
+ RecipientBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) override;
+ std::vector detachPeer(MacAddress peer) override;
virtual ~RecipientBlockAckAgreementHandler();
virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) override;
virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override;
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc
index 73813feaa5d..f2bd97fa697 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc
+++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc
@@ -19,12 +19,27 @@ void RecipientBlockAckAgreementPolicy::initialize(int stage)
isAMsduSupported = par("aMsduSupported");
maximumAllowedBufferSize = par("maximumAllowedBufferSize");
blockAckTimeoutValue = par("blockAckTimeoutValue");
+ if (maximumAllowedBufferSize <= 0 || blockAckTimeoutValue < SIMTIME_ZERO)
+ throw cRuntimeError("Invalid Block Ack buffer size or inactivity timeout");
}
}
bool RecipientBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaReq)
{
- return true;
+ // IEEE Std 802.11-2024, 10.25.2 and Table 9-466: validate ordinary setup; the requested buffer size is advisory.
+ // INET supports immediate Block Ack for historical Basic compatibility and selects at most 64 entries in the response.
+ return !addbaReq->getTransmitterAddress().isUnspecified() &&
+ !addbaReq->getTransmitterAddress().isMulticast() &&
+ addbaReq->getTid() >= 0 && addbaReq->getTid() <= 15 &&
+ addbaReq->getDialogToken() > 0 && addbaReq->getDialogToken() <= 255 &&
+ addbaReq->getBlockAckPolicy() && addbaReq->getBufferSize() >= 0 &&
+ addbaReq->getBufferSize() <= 1023 && addbaReq->getStartingSequenceNumber().isSpecified() &&
+ addbaReq->getBlockAckTimeoutValue() >= SIMTIME_ZERO;
+}
+
+bool RecipientBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const
+{
+ return variant == BlockAckVariant::BASIC || (localHt && peerHt);
}
bool RecipientBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba)
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h
index 995fcc29fdb..d74bfd4240b 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h
+++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h
@@ -28,6 +28,7 @@ class INET_API RecipientBlockAckAgreementPolicy : public SimpleModule, public IR
virtual void initialize(int stage) override;
public:
+ virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override;
virtual bool isAddbaReqAccepted(const Ptr& addbaReq) override;
virtual bool isDelbaAccepted(const Ptr& delba) override;
diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned
index 8c41285b009..6fb403594cf 100644
--- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned
+++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned
@@ -20,7 +20,7 @@ simple RecipientBlockAckAgreementPolicy extends SimpleModule like IRecipientBloc
bool delayedAckPolicySupported = default(false);
bool aMsduSupported = default(true);
int maximumAllowedBufferSize = default(64);
- double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator
+ double blockAckTimeoutValue @unit(s) = default(0s); // Zero disables inactivity expiration.
@display("i=block/control");
}
diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc
index 6cffea3c37d..02a811e4732 100644
--- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc
+++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc
@@ -12,6 +12,44 @@
namespace inet {
namespace ieee80211 {
+BlockAckReordering::ReleasedFrames BlockAckReordering::detachAgreement(MacAddress peer, Tid tid,
+ bool deliverComplete, std::vector& discarded)
+{
+ auto it = receiveBuffers.find({tid, peer});
+ if (it == receiveBuffers.end())
+ return {};
+ auto buffer = it->second;
+ receiveBuffers.erase(it);
+ ReleasedFrames released;
+ auto start = buffer->getNextExpectedSequenceNumber();
+ for (int i = 0; i < buffer->getBufferSize(); i++) {
+ auto sequence = start + i;
+ auto position = buffer->getBuffer().find(sequence.get());
+ if (position == buffer->getBuffer().end())
+ continue;
+ auto fragments = position->second;
+ if (deliverComplete && isComplete(fragments))
+ released.push_back({sequence.get(), fragments});
+ else
+ discarded.insert(discarded.end(), fragments.begin(), fragments.end());
+ buffer->removeFrame(sequence);
+ }
+ delete buffer;
+ return released;
+}
+
+void BlockAckReordering::clearPeer(MacAddress peer)
+{
+ for (auto it = receiveBuffers.begin(); it != receiveBuffers.end();) {
+ if (it->first.second == peer) {
+ delete it->second;
+ it = receiveBuffers.erase(it);
+ }
+ else
+ ++it;
+ }
+}
+
BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader)
{
ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement);
diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h
index 42215dd9b40..05a203ed44b 100644
--- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h
+++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h
@@ -50,6 +50,8 @@ class INET_API BlockAckReordering
public:
virtual ~BlockAckReordering();
+ ReleasedFrames detachAgreement(MacAddress peer, Tid tid, bool deliverComplete, std::vector& discarded);
+ void clearPeer(MacAddress peer);
void processReceivedDelba(const Ptr& delba);
ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader);
diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc
index 5dc48059089..cb0e9d6549b 100644
--- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc
+++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc
@@ -165,6 +165,11 @@ bool Edcaf::isInternalCollision()
return collisionController->isInternalCollision(this);
}
+bool Edcaf::hasEligibleFrames() const
+{
+ return frameEligibility ? frameEligibility() : !pendingQueue->isEmpty() || inProgressFrames->getLength() != 0;
+}
+
int Edcaf::getCwMax(AccessCategory ac, int aCwMax, int aCwMin)
{
switch (ac) {
diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h
index c2d716ade33..b9a77e9c051 100644
--- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h
+++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h
@@ -49,6 +49,7 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub
InProgressFrames *inProgressFrames = nullptr;
bool owning = false;
+ std::function frameEligibility;
simtime_t slotTime = -1;
simtime_t sifs = -1;
@@ -102,6 +103,8 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub
// Edcaf
virtual bool isOwning() { return owning; }
virtual bool isInternalCollision();
+ void setFrameEligibility(std::function predicate) { frameEligibility = predicate; }
+ bool hasEligibleFrames() const;
virtual AccessCategory getAccessCategory() { return ac; }
};
diff --git a/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc b/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc
index 33b94c93563..4b3172e039f 100644
--- a/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc
+++ b/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc
@@ -36,9 +36,9 @@ bool EdcaCollisionController::isInternalCollision(Edcaf *edcaf)
simtime_t now = simTime();
AccessCategory accessCategory = edcaf->getAccessCategory();
auto hasFrames = [](const Edcaf *candidate) {
- return !candidate->getPendingQueue()->isEmpty() || candidate->getInProgressFrames()->getLength() != 0;
+ return candidate->hasEligibleFrames();
};
- // An expired empty queue cannot initiate a TXOP or cause an internal collision.
+ // Deferred or expired DATA cannot initiate a TXOP or cause an internal collision.
// Keep the winner's in-progress frame visible until its transmission completes.
if (txStartTimes[accessCategory] == now && hasFrames(edcaf)) {
for (int ac = accessCategory + 1; ac < 4; ac++) {
diff --git a/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h b/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h
new file mode 100644
index 00000000000..f02e1033036
--- /dev/null
+++ b/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#ifndef __INET_BLOCKACKVARIANT_H
+#define __INET_BLOCKACKVARIANT_H
+
+namespace inet {
+namespace ieee80211 {
+
+enum class BlockAckVariant { BASIC, COMPRESSED };
+
+} // namespace ieee80211
+} // namespace inet
+
+#endif
diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h
index cc4a7b86b92..3fefe1666c0 100644
--- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h
+++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h
@@ -18,7 +18,8 @@ class INET_API IBlockAckAgreementHandlerCallback
public:
virtual ~IBlockAckAgreementHandlerCallback() {}
- virtual void scheduleInactivityTimer(simtime_t timeout) = 0;
+ // The coordinator recomputes the earliest absolute deadline across both agreement roles.
+ virtual void scheduleInactivityTimer() = 0;
};
} // namespace ieee80211
diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h
index ee9d4d37465..0f61eaa9891 100644
--- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h
+++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h
@@ -48,6 +48,8 @@ class INET_API IFrameSequenceHandler
virtual void transmissionComplete() = 0;
virtual bool isSequenceRunning() = 0;
virtual void handleStartRxTimeout() = 0;
+ // The caller can suppress failure feedback for an exchange that its procedure already resolved.
+ virtual void interruptFrameSequence(bool notifyFailure) = 0;
};
} // namespace ieee80211
diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h
index 4a01e89b6c3..7161304e0ad 100644
--- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h
+++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h
@@ -22,7 +22,27 @@ namespace ieee80211 {
class INET_API IOriginatorBlockAckAgreementHandler
{
public:
+ typedef std::pair AgreementId;
+ enum class ProcedureState { SETUP_INTENT, REQUEST_PENDING, TEARDOWN_PENDING, RECOVERY_NEEDED, WAITING_FOR_SUCCESSOR };
+ struct Procedure {
+ ProcedureState state = ProcedureState::SETUP_INTENT;
+ bool requestAttempted = false;
+ simtime_t responseDeadline = SIMTIME_MAX;
+ };
virtual ~IOriginatorBlockAckAgreementHandler() {}
+ virtual const std::map& getAgreements() const = 0;
+ virtual const std::map& getProcedures() const = 0;
+ virtual simtime_t computeEarliestExpirationTime() = 0;
+ virtual OriginatorBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) = 0;
+ virtual std::vector detachPeer(MacAddress peer) = 0;
+ virtual void noteEligibleDataTransmission(MacAddress peer, Tid tid) = 0;
+ virtual void beginSetupIntent(MacAddress peer, Tid tid) = 0;
+ virtual void beginTeardown(MacAddress peer, Tid tid) = 0;
+ virtual void failTeardown(MacAddress peer, Tid tid) = 0;
+ virtual void finishProcedure(MacAddress peer, Tid tid) = 0;
+ virtual void markRequestAttempted(MacAddress peer, Tid tid) = 0;
+ virtual void acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) = 0;
+ virtual void startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) = 0;
virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) = 0;
virtual void processTransmittedAddbaReq(const Ptr& addbaReq) = 0;
diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h
index 40ea82aa95e..c6e1ddd9638 100644
--- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h
+++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h
@@ -9,6 +9,7 @@
#define __INET_IORIGINATORBLOCKACKAGREEMENTPOLICY_H
#include "inet/common/packet/Packet.h"
+#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h"
#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h"
namespace inet {
@@ -21,6 +22,8 @@ class INET_API IOriginatorBlockAckAgreementPolicy
public:
virtual ~IOriginatorBlockAckAgreementPolicy() {}
+ virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const = 0;
+
virtual bool isAddbaReqNeeded(Packet *packet, const Ptr& header) = 0;
virtual bool isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) = 0;
virtual bool isDelbaAccepted(const Ptr& delba) = 0;
diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h
index 357745c3ddb..5c4beb3fe04 100644
--- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h
+++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h
@@ -23,6 +23,13 @@ class INET_API IRecipientBlockAckAgreementHandler
{
public:
virtual ~IRecipientBlockAckAgreementHandler() {}
+ virtual simtime_t computeEarliestExpirationTime() = 0;
+ virtual const std::map, RecipientBlockAckAgreement *>& getAgreements() const = 0;
+ virtual RecipientBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) = 0;
+ virtual std::vector detachPeer(MacAddress peer) = 0;
+ virtual bool isTeardownPending(MacAddress peer, Tid tid) const = 0;
+ virtual void beginTeardown(MacAddress peer, Tid tid) = 0;
+ virtual void finishTeardown(MacAddress peer, Tid tid) = 0;
virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) = 0;
virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0;
diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h
index 48009a67161..2f429ef7169 100644
--- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h
+++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h
@@ -9,6 +9,7 @@
#define __INET_IRECIPIENTBLOCKACKAGREEMENTPOLICY_H
#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h"
+#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h"
namespace inet {
namespace ieee80211 {
@@ -18,6 +19,8 @@ class INET_API IRecipientBlockAckAgreementPolicy
public:
virtual ~IRecipientBlockAckAgreementPolicy() {}
+ virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const = 0;
+
virtual bool isAddbaReqAccepted(const Ptr& addbaReq) = 0;
virtual bool isDelbaAccepted(const Ptr& delba) = 0;
diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h
index 8a3ff4c0b7c..7e97329e94d 100644
--- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h
+++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h
@@ -23,6 +23,8 @@ class INET_API IRecipientQosMacDataService
public:
virtual ~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;
virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0;
diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc
index a05c4f1d269..996908c9b35 100644
--- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc
+++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc
@@ -10,6 +10,7 @@
#include "inet/common/ModuleAccess.h"
#include "inet/common/Simsignals.h"
#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h"
+#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h"
#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h"
#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h"
#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h"
@@ -19,6 +20,9 @@
#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h"
#include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h"
#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h"
+#include "inet/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"
namespace inet {
@@ -46,9 +50,11 @@ void Hcf::initialize(int stage)
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");
inactivityTimer = new cMessage("blockAckInactivityTimer");
dataLifetimeTimer = new cMessage("dataLifetimeTimer");
+ addbaResponseTimer = new cMessage("addbaResponseTimer");
edca = check_and_cast(getSubmodule("edca"));
hcca = check_and_cast(getSubmodule("hcca"));
tx = check_and_cast(getModuleByPath(par("txModule")));
@@ -83,10 +89,17 @@ void Hcf::initialize(int stage)
lifetimeHandler->setDeadlineChangedCallback([this]() { scheduleDataLifetimeTimer(); });
check_and_cast(originatorDataService)->setLifetimeHandler(
lifetimeHandler, [this]() { expireDataFrames(); });
+ check_and_cast(originatorDataService)->setEligibility(
+ [this](const Packet *packet) { return isFrameEligible(packet, false); },
+ [this](Packet *packet) { return isAMsduPermitted(packet); });
+ mib->subscribe(Ieee80211Mib::rateStateChangedSignal, this);
for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) {
+ edca->getEdcaf(static_cast(ac))->setFrameEligibility(
+ [this, ac]() { return hasEligibleFrameToTransmit(static_cast(ac)); });
check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(packetDroppedSignal, this);
auto frames = edca->getEdcaf(static_cast(ac))->getInProgressFrames();
frames->setLifetimeHandler(lifetimeHandler);
+ frames->setTransmissionEligibility([this](Packet *packet) { return isFrameEligible(packet, true); });
frames->subscribe(InProgressFrames::packetDequeuedSignal, this);
}
}
@@ -179,6 +192,8 @@ void Hcf::expireDataFrames(bool preserveSequence)
PacketDropDetails details;
details.setReason(LIFETIME_EXPIRED);
emit(packetDroppedSignal, packet, &details);
+ if (inProgress)
+ firstDataDiscarded(header);
if (!inProgress)
delete packet;
}
@@ -200,8 +215,22 @@ void Hcf::forEachChild(cVisitor *v)
void Hcf::handleMessage(cMessage *msg)
{
- if (msg == dataLifetimeTimer)
+ if (msg == addbaResponseTimer) {
+ std::vector> expired;
+ for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) {
+ if (entry.second.responseDeadline <= simTime())
+ expired.push_back(entry.first);
+ }
+ for (const auto& id : expired)
+ beginTeardown(id.first, id.second, true, 39);
+ scheduleAddbaResponseTimer();
+ updateContention();
+ }
+ else if (msg == dataLifetimeTimer) {
expireDataFrames();
+ updateAgreementProcedures();
+ updateContention();
+ }
else if (msg == startRxTimer) {
if (!isReceptionInProgress()) {
frameSequenceHandler->handleStartRxTimeout();
@@ -209,8 +238,19 @@ void Hcf::handleMessage(cMessage *msg)
}
else if (msg == inactivityTimer) {
if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) {
- originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this);
- recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this);
+ std::vector> expired;
+ for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) {
+ if (entry.second->getIsAddbaResponseReceived() && entry.second->getExpirationTime() <= simTime())
+ expired.push_back({entry.first.first, entry.first.second, true});
+ }
+ for (const auto& entry : recipientBlockAckAgreementHandler->getAgreements()) {
+ if (entry.second->getExpirationTime() <= simTime())
+ expired.push_back({entry.first.first, entry.first.second, false});
+ }
+ for (const auto& entry : expired)
+ beginTeardown(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry), 39);
+ scheduleInactivityTimer();
+ updateContention();
}
else
throw cRuntimeError("Unknown event");
@@ -255,7 +295,8 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetType() == ST_DATA_WITH_QOS)
edca->getLifetimeHandler()->registerFrame(packet, ac);
pendingQueue->enqueuePacket(packet);
- if (!pendingQueue->isEmpty()) {
+ updateAgreementProcedures();
+ if (!pendingQueue->isEmpty() && !handlingAgreementAction && hasFrameToTransmit(ac)) {
auto edcaf = edca->getChannelOwner();
if (edcaf == nullptr || edcaf->getAccessCategory() != ac) {
EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl;
@@ -274,6 +315,7 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj,
Enter_Method("%s", cComponent::getSignalName(signalID));
auto packet = check_and_cast(obj);
edca->getLifetimeHandler()->forgetFrame(packet->getId());
+ actionTerminated(packet, false);
if (packet->findTag() != nullptr) {
FrameTransmissionDetails transmissionDetails;
transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION);
@@ -290,10 +332,423 @@ void Hcf::scheduleStartRxTimer(simtime_t timeout)
scheduleAfter(timeout, startRxTimer);
}
-void Hcf::scheduleInactivityTimer(simtime_t timeout)
+void Hcf::scheduleInactivityTimer()
{
Enter_Method("scheduleInactivityTimer");
- rescheduleAfter(timeout, inactivityTimer);
+ // IEEE Std 802.11-2024, 11.5.4 requires inactivity expiration. HCF reads each role's absolute deadline.
+ simtime_t deadline = SIMTIME_MAX;
+ if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler)
+ deadline = std::min(originatorBlockAckAgreementHandler->computeEarliestExpirationTime(),
+ recipientBlockAckAgreementHandler->computeEarliestExpirationTime());
+ cancelEvent(inactivityTimer);
+ if (deadline != SIMTIME_MAX)
+ scheduleAt(std::max(simTime(), deadline), inactivityTimer);
+}
+
+void Hcf::scheduleAddbaResponseTimer()
+{
+ simtime_t deadline = SIMTIME_MAX;
+ if (originatorBlockAckAgreementHandler) {
+ for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures())
+ deadline = std::min(deadline, entry.second.responseDeadline);
+ }
+ cancelEvent(addbaResponseTimer);
+ if (deadline != SIMTIME_MAX)
+ scheduleAt(std::max(simTime(), deadline), addbaResponseTimer);
+}
+
+bool Hcf::isProfileAllowed(MacAddress peer, bool originator) const
+{
+ auto state = mib->findPeerHtState(peer);
+ bool peerHt = state && state->valid && (originator ? state->negotiatedCapabilities.localTxPeerRx.valid :
+ state->negotiatedCapabilities.localRxPeerTx.valid);
+ return originator ? originatorBlockAckAgreementPolicy->isProfileAllowed(blockAckVariant, mib->isHtOperationSupported(), peerHt) :
+ recipientBlockAckAgreementPolicy->isProfileAllowed(blockAckVariant, mib->isHtOperationSupported(), peerHt);
+}
+
+bool Hcf::hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const
+{
+ if (finishingSequence || !frameSequenceHandler->isSequenceRunning())
+ return false;
+ auto context = frameSequenceHandler->getContext();
+ for (int i = 0; i < context->getNumSteps(); i++) {
+ auto step = dynamic_cast(context->getStep(i));
+ if (!step)
+ continue;
+ auto packet = step->getFrameToTransmit();
+ if (!obsoleteActions.count(packet->getId()))
+ continue;
+ auto header = packet->peekAtFront();
+ if (header->getReceiverAddress() != peer)
+ continue;
+ if (auto request = dynamicPtrCast(header)) {
+ if (originator && request->getTid() == tid)
+ return true;
+ }
+ else if (auto response = dynamicPtrCast(header)) {
+ if (!originator && response->getTid() == tid)
+ return true;
+ }
+ else if (auto delba = dynamicPtrCast(header)) {
+ if (delba->getInitiator() == originator && delba->getTid() == tid)
+ return true;
+ }
+ }
+ return false;
+}
+
+bool Hcf::isFrameEligible(const Packet *packet, bool numbered) const
+{
+ if (obsoleteActions.count(packet->getId()))
+ return false;
+ auto data = dynamicPtrCast(packet->peekAtFront());
+ if (!data || data->getType() != ST_DATA_WITH_QOS || !originatorBlockAckAgreementHandler)
+ return true;
+ auto peer = data->getReceiverAddress();
+ auto tid = data->getTid();
+ if (hasObsoleteActionInFlight(peer, tid, true))
+ return false;
+ const auto& procedures = originatorBlockAckAgreementHandler->getProcedures();
+ auto it = procedures.find({peer, tid});
+ if (it == procedures.end())
+ return true;
+ if (it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::WAITING_FOR_SUCCESSOR)
+ return true;
+ return numbered && it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::SETUP_INTENT;
+}
+
+bool Hcf::isAMsduPermitted(Packet *packet) const
+{
+ auto data = dynamicPtrCast(packet->peekAtFront());
+ if (!data || !originatorBlockAckAgreementHandler)
+ return true;
+ auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid());
+ return !agreement || !agreement->getIsAddbaResponseReceived() || agreement->isAMsduPermitted();
+}
+
+bool Hcf::hasQueuedData(MacAddress peer, Tid tid) const
+{
+ queueing::CPacketFilterFunction filter([this, peer, tid](const Packet *packet) {
+ auto data = dynamicPtrCast(packet->peekAtFront());
+ 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++) {
+ auto queue = edca->getEdcaf(static_cast(ac))->getPendingQueue();
+ if (queue->isEmpty())
+ continue;
+ auto module = dynamic_cast(queue);
+ if (queue->canPullPacket(module ? module->gate("out") : nullptr, &filter))
+ return true;
+ }
+ return false;
+}
+
+bool Hcf::hasNumberedData(MacAddress peer, Tid tid) const
+{
+ 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->getReceiverAddress() == peer && data->getTid() == tid)
+ return true;
+ }
+ }
+ return false;
+}
+
+void Hcf::updateAgreementProcedures()
+{
+ if (!originatorBlockAckAgreementHandler || updatingProcedures)
+ return;
+ updatingProcedures = true;
+ auto procedures = originatorBlockAckAgreementHandler->getProcedures();
+ for (const auto& entry : procedures) {
+ auto peer = entry.first.first;
+ auto tid = entry.first.second;
+ auto state = entry.second.state;
+ using State = IOriginatorBlockAckAgreementHandler::ProcedureState;
+ bool queued = hasQueuedData(peer, tid);
+ // INET setup policy waits for a successor after the first eligible DATA transmission.
+ if (state == State::WAITING_FOR_SUCCESSOR) {
+ if (!queued)
+ continue;
+ originatorBlockAckAgreementHandler->beginSetupIntent(peer, tid);
+ state = State::SETUP_INTENT;
+ }
+ if (state == State::SETUP_INTENT) {
+ if (!queued)
+ originatorBlockAckAgreementHandler->finishProcedure(peer, tid);
+ else if (!hasNumberedData(peer, tid) && !hasObsoleteActionInFlight(peer, tid, true)) {
+ auto service = check_and_cast(originatorDataService);
+ originatorBlockAckAgreementHandler->startSetup(peer, tid, service->getNextSequenceNumber(peer, tid),
+ originatorBlockAckAgreementPolicy, this, service->hasAMsduAggregation());
+ }
+ }
+ else if (state == State::REQUEST_PENDING && !queued) {
+ if (entry.second.requestAttempted)
+ beginTeardown(peer, tid, true, 37);
+ else {
+ originatorBlockAckAgreementHandler->finishProcedure(peer, tid);
+ retireAgreement(peer, tid, true);
+ }
+ }
+ else if (state == State::RECOVERY_NEEDED && queued)
+ beginTeardown(peer, tid, true, 37);
+ }
+ updatingProcedures = false;
+ scheduleAddbaResponseTimer();
+}
+
+void Hcf::updateContention()
+{
+ if (handlingAgreementAction)
+ return;
+ for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) {
+ auto edcaf = edca->getEdcaf(static_cast(ac));
+ if (edcaf->isOwning())
+ continue;
+ if (hasFrameToTransmit(static_cast(ac)))
+ edcaf->requestChannel(this);
+ }
+}
+
+void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex)
+{
+ for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) {
+ auto edcaf = edca->getEdcaf(static_cast(ac));
+ auto frames = edcaf->getInProgressFrames();
+ std::vector discard;
+ for (int i = 0; i < frames->getLength(); i++) {
+ auto packet = frames->getFrames(i);
+ auto data = dynamicPtrCast(packet->peekAtFront());
+ bool transmitted = data && edcaf->getAckHandler()->getQoSDataAckStatus(data) != QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED;
+ if (data && frameSequenceHandler->isSequenceRunning()) {
+ auto context = frameSequenceHandler->getContext();
+ for (int j = 0; j < context->getNumSteps(); j++) {
+ if (auto step = dynamic_cast(context->getStep(j)))
+ transmitted |= step->getFrameToTransmit() == packet;
+ }
+ }
+ // IEEE Std 802.11-2024, 11.5.3.4: the next MPDU must follow the highest transmitted sequence.
+ // Include unsent fragments even if ACK processing already removed an earlier fragment.
+ // Local allocation indices preserve order across gaps and wire sequence wraparound.
+ if (data && data->getReceiverAddress() == peer && data->getTid() == tid &&
+ (all || transmitted || (highestTransmittedDataIndex >= 0 &&
+ packet->getTag()->getAllocationIndex() <= highestTransmittedDataIndex)))
+ discard.push_back(packet);
+ }
+ for (auto packet : discard) {
+ auto data = packet->peekAtFront();
+ edcaf->getAckHandler()->dropFrame(data);
+ edcaf->getRecoveryProcedure()->dropFrame(data);
+ frames->dropFrame(packet);
+ PacketDropDetails details;
+ details.setReason(OTHER_PACKET_DROP);
+ emit(packetDroppedSignal, packet, &details);
+ }
+ if (all) {
+ auto queue = edcaf->getPendingQueue();
+ discard.clear();
+ for (int i = 0; i < queue->getNumPackets(); i++) {
+ auto packet = queue->getPacket(i);
+ auto data = dynamicPtrCast(packet->peekAtFront());
+ if (data && data->getReceiverAddress() == peer && data->getTid() == tid)
+ discard.push_back(packet);
+ }
+ for (auto packet : discard) {
+ queue->removePacket(packet);
+ take(packet);
+ edca->getLifetimeHandler()->forgetFrame(packet->getId());
+ PacketDropDetails details;
+ details.setReason(OTHER_PACKET_DROP);
+ emit(packetDroppedSignal, packet, &details);
+ delete packet;
+ }
+ }
+ if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames)
+ frames->clearDroppedFrames();
+ }
+}
+
+void Hcf::firstDataDiscarded(const Ptr& header)
+{
+ if (!originatorBlockAckAgreementHandler)
+ return;
+ auto agreement = originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid());
+ if (agreement && agreement->getIsAddbaResponseReceived() && !agreement->getFirstDataTransmitted() &&
+ agreement->getStartingSequenceNumber() == header->getSequenceNumber())
+ beginTeardown(header->getReceiverAddress(), header->getTid(), true, 37);
+}
+
+void Hcf::invalidateActions(MacAddress peer, Tid tid, bool originator)
+{
+ std::set invalid;
+ std::set attempted;
+ std::set rtsRecoveryOwned;
+ for (auto it = ownedActions.begin(); it != ownedActions.end();) {
+ if (it->second.peer == peer && it->second.tid == tid && it->second.originator == originator) {
+ invalid.insert(it->first);
+ if (it->second.attempted)
+ attempted.insert(it->first);
+ if (it->second.rtsRecoveryOwned)
+ rtsRecoveryOwned.insert(it->first);
+ obsoleteActions.insert(it->first);
+ it = ownedActions.erase(it);
+ }
+ else
+ ++it;
+ }
+ for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) {
+ auto edcaf = edca->getEdcaf(static_cast(ac));
+ auto queue = edcaf->getPendingQueue();
+ std::vector removed;
+ for (int i = 0; i < queue->getNumPackets(); i++) {
+ if (invalid.count(queue->getPacket(i)->getId()))
+ removed.push_back(queue->getPacket(i));
+ }
+ for (auto packet : removed) {
+ queue->removePacket(packet);
+ take(packet);
+ obsoleteActions.erase(packet->getId());
+ delete packet;
+ }
+ auto frames = edcaf->getInProgressFrames();
+ removed.clear();
+ for (int i = 0; i < frames->getLength(); i++) {
+ if (invalid.count(frames->getFrames(i)->getId()))
+ removed.push_back(frames->getFrames(i));
+ }
+ for (auto packet : removed) {
+ auto header = packet->peekAtFront();
+ edcaf->getAckHandler()->dropFrame(header);
+ if (attempted.count(packet->getId()))
+ edcaf->getMgmtAndNonQoSRecoveryProcedure()->dropFrame(packet, header);
+ if (rtsRecoveryOwned.count(packet->getId()))
+ edcaf->getMgmtAndNonQoSRecoveryProcedure()->dropRtsProtectedFrame(header);
+ frames->dropFrame(packet);
+ if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames)
+ obsoleteActions.erase(packet->getId());
+ }
+ if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames)
+ frames->clearDroppedFrames();
+ }
+}
+
+void Hcf::retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset)
+{
+ if (originator) {
+ auto agreement = originatorBlockAckAgreementHandler->detachAgreement(peer, tid);
+ invalidateActions(peer, tid, originator);
+ discardAgreementData(peer, tid, associationReset, agreement ? agreement->getHighestTransmittedDataIndex() : -1);
+ if (agreement) {
+ if (agreement->getIsAddbaResponseReceived())
+ emit(blockAckAgreementDeletedSignal, agreement);
+ delete agreement;
+ }
+ }
+ else {
+ auto agreement = recipientBlockAckAgreementHandler->detachAgreement(peer, tid);
+ invalidateActions(peer, tid, originator);
+ if (agreement) {
+ sendUp(recipientDataService->retireAgreement(agreement, associationReset));
+ emit(blockAckAgreementDeletedSignal, agreement);
+ delete agreement;
+ }
+ }
+ scheduleInactivityTimer();
+}
+
+void Hcf::beginTeardown(MacAddress peer, Tid tid, bool originator, int reasonCode)
+{
+ if (!originatorBlockAckAgreementHandler || !recipientBlockAckAgreementHandler)
+ return;
+ if (originator) {
+ const auto& procedures = originatorBlockAckAgreementHandler->getProcedures();
+ auto it = procedures.find({peer, tid});
+ if (it != procedures.end() && it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING)
+ return;
+ originatorBlockAckAgreementHandler->beginTeardown(peer, tid);
+ }
+ else {
+ if (recipientBlockAckAgreementHandler->isTeardownPending(peer, tid))
+ return;
+ recipientBlockAckAgreementHandler->beginTeardown(peer, tid);
+ }
+ retireAgreement(peer, tid, originator);
+ auto delba = makeShared();
+ delba->setReceiverAddress(peer);
+ delba->setTid(tid);
+ delba->setInitiator(originator);
+ delba->setReasonCode(reasonCode);
+ processMgmtFrame(new Packet("Delba", delba), delba);
+ scheduleAddbaResponseTimer();
+}
+
+void Hcf::actionTerminated(Packet *packet, bool acknowledged)
+{
+ auto it = ownedActions.find(packet->getId());
+ if (it == ownedActions.end())
+ return;
+ auto action = it->second;
+ ownedActions.erase(it);
+ if (action.rtsRecoveryOwned)
+ edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure()->dropRtsProtectedFrame(packet->peekAtFront());
+ if (action.kind == OwnedAction::REQUEST) {
+ auto agreement = originatorBlockAckAgreementHandler->getAgreement(action.peer, action.tid);
+ if (!agreement || agreement->getIsAddbaResponseReceived())
+ return;
+ if (acknowledged)
+ originatorBlockAckAgreementHandler->acknowledgeRequest(action.peer, action.tid,
+ originatorBlockAckAgreementPolicy->computeAddbaFailureTimeout());
+ else if (action.attempted)
+ // INET recovery policy confirms peer teardown before another setup.
+ beginTeardown(action.peer, action.tid, true, 37);
+ else {
+ originatorBlockAckAgreementHandler->finishProcedure(action.peer, action.tid);
+ retireAgreement(action.peer, action.tid, true);
+ }
+ }
+ else if (action.kind == OwnedAction::DELBA) {
+ if (action.originator) {
+ if (acknowledged)
+ originatorBlockAckAgreementHandler->finishProcedure(action.peer, action.tid);
+ else {
+ // INET recovery policy discards this batch after failed DELBA delivery.
+ originatorBlockAckAgreementHandler->failTeardown(action.peer, action.tid);
+ discardAgreementData(action.peer, action.tid, true);
+ }
+ }
+ else
+ recipientBlockAckAgreementHandler->finishTeardown(action.peer, action.tid);
+ }
+ scheduleAddbaResponseTimer();
+ if (action.kind == OwnedAction::DELBA || (action.kind == OwnedAction::REQUEST && !acknowledged)) {
+ updateAgreementProcedures();
+ updateContention();
+ }
+}
+
+void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, bool value, cObject *details)
+{
+ if (signalID != Ieee80211Mib::rateStateChangedSignal)
+ return;
+ Enter_Method("rateStateChanged");
+ // This cleanup prepares Compressed support. Basic agreements do not depend on HT admission state.
+ if (blockAckVariant == BlockAckVariant::BASIC || !originatorBlockAckAgreementHandler)
+ return;
+ std::vector> retired;
+ for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) {
+ if (!isProfileAllowed(entry.first.first, true))
+ retired.push_back({entry.first.first, entry.first.second, true});
+ }
+ for (const auto& entry : recipientBlockAckAgreementHandler->getAgreements()) {
+ if (!isProfileAllowed(entry.first.first, false))
+ retired.push_back({entry.first.first, entry.first.second, false});
+ }
+ for (const auto& entry : retired)
+ beginTeardown(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry), 37);
+ updateContention();
}
void Hcf::processLowerFrame(Packet *packet, const Ptr& header)
@@ -302,6 +757,33 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr&
take(packet);
EV_INFO << "Processing lower frame: " << packet->getName() << endl;
auto edcaf = edca->getChannelOwner();
+ auto action = dynamicPtrCast(header);
+ bool blockAckAction = dynamicPtrCast(header) ||
+ dynamicPtrCast(header) || dynamicPtrCast(header);
+ if (action && blockAckAction && isForUs(header) && edcaf && frameSequenceHandler->isSequenceRunning() &&
+ dynamic_cast(frameSequenceHandler->getContext()->getLastStep())) {
+ handlingAgreementAction = true;
+ emit(packetReceivedFromPeerSignal, packet);
+ std::unique_ptr responseSource(packet->dup());
+ auto outgoing = check_and_cast(frameSequenceHandler->getContext()->getStepBeforeLast())->getFrameToTransmit();
+ auto accepted = recipientDataService->managementFrameReceived(packet, action);
+ for (auto frame : accepted) {
+ take(frame);
+ dispatchAgreementAction(frame->peekAtFront());
+ }
+ cancelEvent(startRxTimer);
+ bool resolved = obsoleteActions.count(outgoing->getId()) ||
+ (dynamicPtrCast(outgoing->peekAtFront()) &&
+ !edcaf->getInProgressFrames()->containsFrame(outgoing));
+ frameSequenceHandler->interruptFrameSequence(!resolved);
+ recipientAckProcedure->processReceivedFrame(responseSource.get(), action, check_and_cast(recipientAckPolicy), this);
+ for (auto frame : accepted)
+ delete frame;
+ handlingAgreementAction = false;
+ updateAgreementProcedures();
+ updateContention();
+ return;
+ }
if (edcaf && frameSequenceHandler->isSequenceRunning()) {
// TODO always call processResponse?
if ((!isForUs(header) && !startRxTimer->isScheduled()) || isForUs(header)) {
@@ -420,6 +902,8 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs)
transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED);
emit(Ieee80211Mac::frameTransmissionOutcomeSignal, internallyCollidedFrame, &transmissionDetails);
}
+ else if (auto dataHeader = dynamicPtrCast(internallyCollidedHeader))
+ firstDataDiscarded(dataHeader);
if (hasFrameToTransmit(ac))
edcaf->requestChannel(this);
}
@@ -437,12 +921,14 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs)
void Hcf::frameSequenceFinished()
{
Enter_Method("frameSequenceFinished");
+ finishingSequence = true;
emit(IFrameSequenceHandler::frameSequenceFinishedSignal, frameSequenceHandler->getContext());
auto edcaf = edca->getChannelOwner();
if (edcaf) {
// The context still owns references. dropFrame defers deletion until context disposal.
expireDataFrames(false);
- bool startContention = hasFrameToTransmit(); // TODO outstanding frame
+ updateAgreementProcedures();
+ bool startContention = !handlingAgreementAction && hasFrameToTransmit();
edcaf->releaseChannel(this);
mac->sendDownPendingRadioConfigMsg(); // TODO review
edcaf->getTxopProcedure()->endTxop();
@@ -457,12 +943,51 @@ void Hcf::frameSequenceFinished()
}
else
throw cRuntimeError("Frame sequence finished but channel owner not found!");
+ obsoleteActions.clear();
+ finishingSequence = false;
}
void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header)
{
EV_INFO << "Processing received frame " << packet->getName() << " as recipient.\n";
emit(packetReceivedFromPeerSignal, packet);
+ if (recipientBlockAckAgreementHandler) {
+ // IEEE Std 802.11-2024, 11.5.4: valid Block Ack traffic without an agreement requires DELBA.
+ // Reject malformed DATA and unsupported BAR fields before this recovery path.
+ if (auto data = dynamicPtrCast(header)) {
+ 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())) {
+ delete packet;
+ return;
+ }
+ if (data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == BLOCK_ACK &&
+ !recipientBlockAckAgreementHandler->getAgreement(data->getTid(), data->getTransmitterAddress())) {
+ beginTeardown(data->getTransmitterAddress(), data->getTid(), false, 38);
+ PacketDropDetails details;
+ details.setReason(OTHER_PACKET_DROP);
+ emit(packetDroppedSignal, packet, &details);
+ delete packet;
+ return;
+ }
+ }
+ else if (auto bar = dynamicPtrCast(header)) {
+ if (bar->getFragmentNumber() != 0 || bar->getMultiTid() || bar->getCompressedBitmap() ||
+ bar->getTidInfo() < 0 || bar->getTidInfo() > 15 || !bar->getStartingSequenceNumber().isSpecified() ||
+ bar->getTransmitterAddress().isUnspecified() || bar->getTransmitterAddress().isMulticast()) {
+ delete packet;
+ return;
+ }
+ auto agreement = recipientBlockAckAgreementHandler->getAgreement(bar->getTidInfo(), bar->getTransmitterAddress());
+ if (!agreement) {
+ beginTeardown(bar->getTransmitterAddress(), bar->getTidInfo(), false, 38);
+ delete packet;
+ return;
+ }
+ agreement->calculateExpirationTime();
+ scheduleInactivityTimer();
+ }
+ }
if (auto dataOrMgmtHeader = dynamicPtrCast(header))
recipientAckProcedure->processReceivedFrame(packet, dataOrMgmtHeader, check_and_cast(recipientAckPolicy), this);
if (auto dataHeader = dynamicPtrCast(header)) {
@@ -471,8 +996,19 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const PtrdataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler));
}
else if (auto mgmtHeader = dynamicPtrCast(header)) {
- sendUp(recipientDataService->managementFrameReceived(packet, mgmtHeader));
- recipientProcessReceivedManagementFrame(mgmtHeader);
+ auto accepted = recipientDataService->managementFrameReceived(packet, mgmtHeader);
+ for (auto frame : accepted) {
+ take(frame);
+ auto completeHeader = frame->peekAtFront();
+ if (dynamicPtrCast(completeHeader)) {
+ recipientProcessReceivedManagementFrame(completeHeader);
+ delete frame;
+ }
+ else
+ sendUp({frame});
+ }
+ updateAgreementProcedures();
+ updateContention();
}
else { // TODO else if (auto ctrlFrame = dynamic_cast(frame))
sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler));
@@ -496,35 +1032,76 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header)
+{
+ dispatchAgreementAction(header);
+}
+
+bool Hcf::dispatchAgreementAction(const Ptr& header)
{
if (recipientBlockAckAgreementHandler && originatorBlockAckAgreementHandler) {
if (auto addbaRequest = dynamicPtrCast(header)) {
+ auto peer = addbaRequest->getTransmitterAddress();
+ auto tid = addbaRequest->getTid();
+ auto previous = recipientBlockAckAgreementHandler->getAgreement(tid, peer);
+ if (!isProfileAllowed(peer, false) || hasObsoleteActionInFlight(peer, tid, false)) {
+ auto response = makeShared();
+ response->setReceiverAddress(peer);
+ response->setTid(tid);
+ response->setDialogToken(addbaRequest->getDialogToken());
+ response->setStatusCode(SC_UNSPECIFIED);
+ processMgmtFrame(new Packet("AddbaResponse", response), response);
+ return false;
+ }
recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this);
auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress());
- emit(blockAckAgreementAddedSignal, agreement);
+ if (!previous && agreement)
+ emit(blockAckAgreementAddedSignal, agreement);
+ scheduleInactivityTimer();
}
else if (auto addbaResp = dynamicPtrCast(header)) {
+ auto peer = addbaResp->getTransmitterAddress();
+ auto tid = addbaResp->getTid();
+ auto pending = originatorBlockAckAgreementHandler->getAgreement(peer, tid);
+ if (!pending || pending->getIsAddbaResponseReceived() || pending->getDialogToken() != addbaResp->getDialogToken())
+ return false;
+ if (addbaResp->getStatusCode() != SC_SUCCESSFUL) {
+ originatorBlockAckAgreementHandler->finishProcedure(peer, tid);
+ retireAgreement(peer, tid, true);
+ scheduleAddbaResponseTimer();
+ return true;
+ }
+ if (!isProfileAllowed(peer, true) || !originatorBlockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, pending))
+ return false;
originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this);
auto agreement = originatorBlockAckAgreementHandler->getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid());
+ originatorBlockAckAgreementHandler->finishProcedure(peer, tid);
+ invalidateActions(peer, tid, true);
emit(blockAckAgreementAddedSignal, agreement);
+ scheduleAddbaResponseTimer();
+ if (!hasQueuedData(peer, tid) && !hasNumberedData(peer, tid))
+ beginTeardown(peer, tid, true, 37);
+ return true;
}
else if (auto delba = dynamicPtrCast(header)) {
- if (delba->getInitiator()) {
- auto agreement = recipientBlockAckAgreementHandler->getAgreement(delba->getTid(), delba->getReceiverAddress());
- emit(blockAckAgreementDeletedSignal, agreement);
- recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy);
- }
- else {
- auto agreement = originatorBlockAckAgreementHandler->getAgreement(delba->getReceiverAddress(), delba->getTid());
- emit(blockAckAgreementDeletedSignal, agreement);
- originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy);
- }
+ auto peer = delba->getTransmitterAddress();
+ auto tid = delba->getTid();
+ bool originator = !delba->getInitiator();
+ if (tid < 0 || tid > 15 || peer.isMulticast() || peer.isUnspecified())
+ return false;
+ if (originator)
+ originatorBlockAckAgreementHandler->finishProcedure(peer, tid);
+ else
+ recipientBlockAckAgreementHandler->finishTeardown(peer, tid);
+ retireAgreement(peer, tid, originator);
+ scheduleAddbaResponseTimer();
+ return true;
}
else
; // Beacon, etc
}
else
; // Optional modules
+ return false;
}
void Hcf::transmissionComplete(Packet *packet, const Ptr& header)
@@ -550,6 +1127,8 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet)
Enter_Method("originatorProcessRtsProtectionFailed");
auto protectedHeader = packet->peekAtFront();
auto edcaf = edca->getChannelOwner();
+ if (obsoleteActions.count(packet->getId()) || !edcaf->getInProgressFrames()->containsFrame(packet))
+ return;
if (edcaf) {
EV_INFO << "RTS frame transmission failed\n";
bool retryLimitReached = false;
@@ -559,6 +1138,9 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet)
}
else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) {
edcaf->getMgmtAndNonQoSRecoveryProcedure()->rtsFrameTransmissionFailed(mgmtHeader, edcaf->getStationRetryCounters());
+ auto action = ownedActions.find(packet->getId());
+ if (action != ownedActions.end())
+ action->second.rtsRecoveryOwned = true;
retryLimitReached = edcaf->getMgmtAndNonQoSRecoveryProcedure()->isRtsFrameRetryLimitReached(packet, mgmtHeader);
}
else
@@ -577,7 +1159,10 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet)
details.setLimit(-1); // TODO
emit(packetDroppedSignal, packet, &details);
emit(linkBrokenSignal, packet);
+ if (auto data = dynamicPtrCast(protectedHeader))
+ firstDataDiscarded(data);
if (dynamicPtrCast(protectedHeader)) {
+ actionTerminated(packet, false);
FrameTransmissionDetails transmissionDetails;
transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED);
emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails);
@@ -591,6 +1176,13 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet)
void Hcf::originatorProcessTransmittedFrame(Packet *packet)
{
Enter_Method("originatorProcessTransmittedFrame");
+ auto edcaf = edca->getChannelOwner();
+ if (edcaf)
+ edcaf->emit(packetSentToPeerSignal, packet);
+ if (obsoleteActions.count(packet->getId()) ||
+ (dynamicPtrCast(packet->peekAtFront()) &&
+ !edca->getChannelOwner()->getInProgressFrames()->containsFrame(packet)))
+ return;
EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n";
auto transmittedHeader = packet->peekAtFront();
if (auto dataOrMgmt = dynamicPtrCast(transmittedHeader)) {
@@ -599,9 +1191,7 @@ void Hcf::originatorProcessTransmittedFrame(Packet *packet)
rx->successfulFrameTransmitted(step->getPpduEnd(), transmittedHeader->getDurationField());
}
}
- auto edcaf = edca->getChannelOwner();
if (edcaf) {
- edcaf->emit(packetSentToPeerSignal, packet);
AccessCategory ac = edcaf->getAccessCategory();
if (transmittedHeader->getReceiverAddress().isMulticast()) {
if (dynamicPtrCast(transmittedHeader))
@@ -630,6 +1220,14 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader);
if (originatorBlockAckAgreementHandler)
originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this);
+ if (originatorBlockAckAgreementHandler && dataHeader->getType() == ST_DATA_WITH_QOS &&
+ !dataHeader->getReceiverAddress().isMulticast() && isProfileAllowed(dataHeader->getReceiverAddress(), true) &&
+ originatorBlockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) &&
+ !originatorBlockAckAgreementHandler->getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid())) {
+ originatorBlockAckAgreementHandler->noteEligibleDataTransmission(dataHeader->getReceiverAddress(), dataHeader->getTid());
+ if (hasQueuedData(dataHeader->getReceiverAddress(), dataHeader->getTid()))
+ originatorBlockAckAgreementHandler->beginSetupIntent(dataHeader->getReceiverAddress(), dataHeader->getTid());
+ }
if (dataHeader->getAckPolicy() == NO_ACK)
edcaf->getInProgressFrames()->dropFrame(packet);
}
@@ -670,6 +1268,9 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket)
Enter_Method("originatorProcessFailedFrame");
auto failedHeader = failedPacket->peekAtFront();
auto edcaf = edca->getChannelOwner();
+ if (obsoleteActions.count(failedPacket->getId()) ||
+ (dynamicPtrCast(failedHeader) && !edcaf->getInProgressFrames()->containsFrame(failedPacket)))
+ return;
if (edcaf) {
bool retryLimitReached = false;
if (auto dataHeader = dynamicPtrCast(failedHeader)) {
@@ -713,6 +1314,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket)
emit(packetDroppedSignal, failedPacket, &details);
emit(linkBrokenSignal, failedPacket);
if (dynamicPtrCast(failedHeader)) {
+ actionTerminated(failedPacket, false);
FrameTransmissionDetails transmissionDetails;
transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED);
emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails);
@@ -732,8 +1334,12 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket)
void Hcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTransmittedPacket)
{
Enter_Method("originatorProcessReceivedFrame");
- EV_INFO << "Processing received frame " << receivedPacket->getName() << " as originator in frame sequence.\n";
emit(packetReceivedFromPeerSignal, receivedPacket);
+ if (obsoleteActions.count(lastTransmittedPacket->getId()) ||
+ (dynamicPtrCast(lastTransmittedPacket->peekAtFront()) &&
+ !edca->getChannelOwner()->getInProgressFrames()->containsFrame(lastTransmittedPacket)))
+ return;
+ EV_INFO << "Processing received frame " << receivedPacket->getName() << " as originator in frame sequence.\n";
auto receivedHeader = receivedPacket->peekAtFront();
auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront();
auto step = check_and_cast(frameSequenceHandler->getContext()->getStepBeforeLast());
@@ -786,6 +1392,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetInProgressFrames()->dropFrame(lastTransmittedPacket);
edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader);
if (dynamicPtrCast(lastTransmittedHeader)) {
+ actionTerminated(lastTransmittedPacket, true);
FrameTransmissionDetails transmissionDetails;
transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED);
emit(Ieee80211Mac::frameTransmissionOutcomeSignal, lastTransmittedPacket, &transmissionDetails);
@@ -833,9 +1440,15 @@ void Hcf::originatorProcessReceivedDataFrame(const PtrgetEdcaf(ac);
if (edcaf)
- return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames();
+ return edcaf->getInProgressFrames()->hasEligibleInProgressFrames() ||
+ check_and_cast(originatorDataService)->peekEligibleFrame(edcaf->getPendingQueue()) != nullptr;
else
throw cRuntimeError("Hcca is unimplemented");
}
@@ -845,7 +1458,7 @@ bool Hcf::hasFrameToTransmit()
expireDataFrames();
auto edcaf = edca->getChannelOwner();
if (edcaf)
- return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames();
+ return hasFrameToTransmit(edcaf->getAccessCategory());
else
throw cRuntimeError("Hcca is unimplemented");
}
@@ -974,6 +1587,12 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs)
Enter_Method("transmitFrame");
auto channelOwner = edca->getChannelOwner();
if (channelOwner) {
+ auto header = packet->peekAtFront();
+ if (dynamicPtrCast(header) &&
+ !channelOwner->getInProgressFrames()->containsFrame(packet))
+ return false;
+ if (!isFrameEligible(packet, true))
+ return false;
auto lifetimeHandler = edca->getLifetimeHandler();
auto step = frameSequenceHandler->getContext()->getLastStep();
auto protectedPacket = dynamic_cast(step);
@@ -981,7 +1600,6 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs)
// IEEE Std 802.11-2024, 10.23.2.12.1: a delayed start cannot exceed DATA lifetime.
if (lifetimeHandler->isExpired(data, simTime() + ifs))
return false;
- auto header = packet->peekAtFront();
auto txop = channelOwner->getTxopProcedure();
auto context = frameSequenceHandler->getContext();
bool scopedSingle = txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && !par("isBlockAckSupported").boolValue();
@@ -1077,6 +1695,19 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs)
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);
+ if (auto dataHeader = dynamicPtrCast(header)) {
+ if (originatorBlockAckAgreementHandler) {
+ auto agreement = originatorBlockAckAgreementHandler->getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid());
+ if (agreement && agreement->getIsAddbaResponseReceived())
+ agreement->recordTransmittedData(packet->getTag()->getAllocationIndex());
+ }
+ }
+ auto action = ownedActions.find(packet->getId());
+ if (action != ownedActions.end()) {
+ action->second.attempted = true;
+ if (action->second.kind == OwnedAction::REQUEST)
+ originatorBlockAckAgreementHandler->markRequestAttempted(action->second.peer, action->second.tid);
+ }
return true;
}
else
@@ -1124,6 +1755,37 @@ void Hcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const
void Hcf::processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader)
{
Enter_Method("processMgmtFrame");
+ OwnedAction action;
+ action.peer = mgmtHeader->getReceiverAddress();
+ bool owned = true;
+ if (auto request = dynamicPtrCast(mgmtHeader)) {
+ action.tid = request->getTid();
+ action.originator = true;
+ action.kind = OwnedAction::REQUEST;
+ }
+ else if (auto response = dynamicPtrCast(mgmtHeader)) {
+ action.tid = response->getTid();
+ action.originator = false;
+ action.kind = OwnedAction::RESPONSE;
+ }
+ else if (auto delba = dynamicPtrCast(mgmtHeader)) {
+ action.tid = delba->getTid();
+ action.originator = delba->getInitiator();
+ action.kind = OwnedAction::DELBA;
+ bool pending = action.originator ?
+ originatorBlockAckAgreementHandler->getProcedures().count({action.peer, action.tid}) != 0 &&
+ originatorBlockAckAgreementHandler->getProcedures().at({action.peer, action.tid}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING :
+ recipientBlockAckAgreementHandler->isTeardownPending(action.peer, action.tid);
+ if (!pending) {
+ beginTeardown(action.peer, action.tid, action.originator, delba->getReasonCode());
+ delete mgmtPacket;
+ return;
+ }
+ }
+ else
+ owned = false;
+ if (owned)
+ ownedActions.emplace(mgmtPacket->getId(), action);
mgmtPacket->insertAtBack(makeShared());
processUpperFrame(mgmtPacket, mgmtHeader);
}
@@ -1167,6 +1829,7 @@ void Hcf::corruptedFrameReceived()
Hcf::~Hcf()
{
+ cancelAndDelete(addbaResponseTimer);
cancelAndDelete(dataLifetimeTimer);
cancelAndDelete(startRxTimer);
cancelAndDelete(inactivityTimer);
diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h
index 4a0537880f8..be0263940de 100644
--- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h
+++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h
@@ -12,6 +12,7 @@
#include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h"
#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h"
#include "inet/linklayer/ieee80211/mac/contract/IAckHandler.h"
+#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h"
#include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h"
#include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h"
#include "inet/linklayer/ieee80211/mac/contract/ICtsPolicy.h"
@@ -41,6 +42,7 @@ namespace inet {
namespace ieee80211 {
class Ieee80211Mac;
+class Ieee80211Mib;
/**
* Implements IEEE 802.11 Hybrid Coordination Function.
@@ -48,7 +50,6 @@ class Ieee80211Mac;
class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public IBlockAckAgreementHandlerCallback, public ModeSetListener
{
private:
- enum class BlockAckVariant { BASIC, COMPRESSED };
BlockAckVariant blockAckVariant = BlockAckVariant::BASIC;
public:
@@ -58,11 +59,26 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler:
protected:
Ieee80211Mac *mac = nullptr;
+ Ieee80211Mib *mib = nullptr;
IRateControl *dataAndMgmtRateControl = nullptr;
cMessage *startRxTimer = nullptr;
cMessage *inactivityTimer = nullptr;
cMessage *dataLifetimeTimer = nullptr;
+ cMessage *addbaResponseTimer = nullptr;
+ struct OwnedAction {
+ MacAddress peer;
+ Tid tid = -1;
+ bool originator = false;
+ enum Kind { REQUEST, RESPONSE, DELBA } kind = REQUEST;
+ bool attempted = false;
+ bool rtsRecoveryOwned = false;
+ };
+ std::map ownedActions;
+ std::set obsoleteActions;
+ bool handlingAgreementAction = false;
+ bool finishingSequence = false;
+ bool updatingProcedures = false;
// Transmission and Reception
IRx *rx = nullptr;
@@ -113,6 +129,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler:
virtual void forEachChild(cVisitor *v) override;
virtual void handleMessage(cMessage *msg) override;
virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override;
+ virtual void receiveSignal(cComponent *source, simsignal_t signalID, bool value, cObject *details) override;
virtual void refreshDisplay() const override;
void startFrameSequence(AccessCategory ac);
@@ -131,10 +148,27 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler:
TxopProcedure *txop, simtime_t ppduDuration);
virtual bool hasFrameToTransmit();
virtual bool hasFrameToTransmit(AccessCategory ac);
+ bool hasEligibleFrameToTransmit(AccessCategory ac) const;
virtual bool isReceptionInProgress();
void scheduleDataLifetimeTimer();
void expireDataFrames(bool preserveSequence = true);
std::set getReferencedDataFrames() const;
+ bool isFrameEligible(const Packet *packet, bool numbered) const;
+ bool isAMsduPermitted(Packet *packet) const;
+ bool hasQueuedData(MacAddress peer, Tid tid) const;
+ bool hasNumberedData(MacAddress peer, Tid tid) const;
+ bool hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const;
+ void updateAgreementProcedures();
+ void updateContention();
+ void scheduleAddbaResponseTimer();
+ void actionTerminated(Packet *packet, bool acknowledged);
+ void invalidateActions(MacAddress peer, Tid tid, bool originator);
+ void retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset = false);
+ void beginTeardown(MacAddress peer, Tid tid, bool originator, int reasonCode);
+ void discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex = -1);
+ void firstDataDiscarded(const Ptr& header);
+ bool dispatchAgreementAction(const Ptr& header);
+ bool isProfileAllowed(MacAddress peer, bool originator) const;
// Recipient
virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header);
@@ -176,7 +210,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler:
virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override;
// IProcedureCallback
- virtual void scheduleInactivityTimer(simtime_t timeout) override;
+ virtual void scheduleInactivityTimer() override;
std::string getFrameSequenceInfo() const;
diff --git a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc
index 88395deef09..098aad01b1c 100644
--- a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc
+++ b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc
@@ -12,6 +12,24 @@
namespace inet {
namespace ieee80211 {
+void QoSDuplicateRemoval::setLastReceived(MacAddress peer, Tid tid, const SequenceNumberCyclic& sequence, int fragment)
+{
+ if (sequence.isSpecified())
+ lastSeenSeqNumCache.insert_or_assign(Key(peer, tid), SequenceControlField(sequence.get(), fragment));
+}
+
+void QoSDuplicateRemoval::clearPeer(MacAddress peer)
+{
+ for (auto it = lastSeenSeqNumCache.begin(); it != lastSeenSeqNumCache.end();) {
+ if (it->first.first == peer)
+ it = lastSeenSeqNumCache.erase(it);
+ else
+ ++it;
+ }
+ lastSeenSharedSeqNumCache.erase(peer);
+ lastSeenTimePriorityManagementSeqNumCache.erase(peer);
+}
+
bool QoSDuplicateRemoval::isDuplicate(const Ptr& header)
{
SequenceControlField seqVal(header->getSequenceNumber().get(), header->getFragmentNumber());
diff --git a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h
index d1075215fbc..4a947d4a9f5 100644
--- a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h
+++ b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h
@@ -28,6 +28,8 @@ class INET_API QoSDuplicateRemoval : public IDuplicateRemoval
Mac2SeqValMap lastSeenTimePriorityManagementSeqNumCache;
public:
+ void setLastReceived(MacAddress peer, Tid tid, const SequenceNumberCyclic& sequence, int fragment);
+ void clearPeer(MacAddress peer);
virtual bool isDuplicate(const Ptr& header) override;
};
diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc
index 2f32f0207b9..ce397baca9e 100644
--- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc
+++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc
@@ -26,8 +26,10 @@ Packet *BasicReassembly::addFragment(Packet *packet)
// Frame is not fragmented
if (!header->getMoreFragments() && header->getFragmentNumber() == 0)
return packet;
- // FIXME temporary fix for mgmt frames
- if (dynamicPtrCast(header))
+ // IEEE Std 802.11-2024, 10.4: dispatch Block Ack Actions only after complete MMPDU reassembly.
+ bool blockAckAction = dynamicPtrCast(header) ||
+ dynamicPtrCast(header) || dynamicPtrCast(header);
+ if (dynamicPtrCast(header) && !blockAckAction)
return packet;
// find entry for this frame
Key key;
diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc
index 66699fadeb0..cc6450101f3 100644
--- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc
+++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc
@@ -14,6 +14,17 @@
namespace inet {
namespace ieee80211 {
+void FrameSequenceHandler::interruptFrameSequence(bool notifyFailure)
+{
+ ASSERT(isSequenceRunning());
+ ASSERT(dynamic_cast(context->getLastStep()));
+ context->getLastStep()->setCompletion(IFrameSequenceStep::Completion::REJECTED);
+ if (notifyFailure)
+ abortFrameSequence();
+ else
+ finishFrameSequence();
+}
+
void FrameSequenceHandler::handleStartRxTimeout()
{
auto lastStep = context->getLastStep();
diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h
index de514c54c9d..fa65175856a 100644
--- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h
+++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h
@@ -35,6 +35,7 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler
virtual void processResponse(Packet *frame) override;
virtual void transmissionComplete() override;
virtual void handleStartRxTimeout() override;
+ virtual void interruptFrameSequence(bool notifyFailure) override;
virtual bool isSequenceRunning() override { return frameSequence != nullptr; }
virtual ~FrameSequenceHandler();
diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc
index 60c875bb16f..3fda1fcd52c 100644
--- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc
+++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc
@@ -150,7 +150,13 @@ IFrameSequenceStep *ManagementAckFs::prepareStep(FrameSequenceContext *context)
{
switch (step) {
case 0: {
- auto packet = context->getInProgressFrames()->getFrameToTransmit();
+ Packet *packet = nullptr;
+ if (context->getQoSContext() && dynamic_cast(context->getLastStep())) {
+ if (auto rts = dynamic_cast(context->getStepBeforeLast()))
+ packet = const_cast(rts->getProtectedFrame());
+ }
+ if (packet == nullptr)
+ packet = context->getInProgressFrames()->getFrameToTransmit();
return new TransmitStep(packet, context->getIfs());
}
case 1: {
@@ -195,7 +201,13 @@ IFrameSequenceStep *ManagementFs::prepareStep(FrameSequenceContext *context)
{
switch (step) {
case 0: {
- auto packet = context->getInProgressFrames()->getFrameToTransmit();
+ Packet *packet = nullptr;
+ if (context->getQoSContext() && dynamic_cast(context->getLastStep())) {
+ if (auto rts = dynamic_cast(context->getStepBeforeLast()))
+ packet = const_cast(rts->getProtectedFrame());
+ }
+ if (packet == nullptr)
+ packet = context->getInProgressFrames()->getFrameToTransmit();
return new TransmitStep(packet, context->getIfs());
}
case 1:
diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc
index 4074ff0004c..2b77ca61c30 100644
--- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc
+++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc
@@ -133,20 +133,25 @@ void NonQosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& header)
{
EV_WARN << "Retry limit reached for " << *packet << ".\n";
- auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber());
- if (packet->getByteLength() >= rtsThreshold) {
- auto it = longRetryCounter.find(id);
- if (it != longRetryCounter.end())
- longRetryCounter.erase(it);
- }
- else {
- auto it = shortRetryCounter.find(id);
- if (it != shortRetryCounter.end())
- shortRetryCounter.erase(it);
- }
+ dropFrame(packet, header);
emit(retryLimitReachedSignal, packet);
}
+void NonQosRecoveryProcedure::dropFrame(Packet *packet, const Ptr& header)
+{
+ auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber());
+ if (packet->getByteLength() >= rtsThreshold)
+ longRetryCounter.erase(id);
+ else
+ shortRetryCounter.erase(id);
+}
+
+void NonQosRecoveryProcedure::dropRtsProtectedFrame(const Ptr& header)
+{
+ auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber());
+ shortRetryCounter.erase(id);
+}
+
//
// The SRC for an MPDU of type Data or MMPDU and the SSRC shall be incremented every
// time transmission of a MAC frame of length less than or equal to dot11RTSThreshold
diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h
index f738e56dde6..83c04085b1b 100644
--- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h
+++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h
@@ -65,6 +65,8 @@ class INET_API NonQosRecoveryProcedure : public SimpleModule, public IRecoveryPr
virtual bool isRtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader);
virtual void retryLimitReached(Packet *packet, const Ptr& header);
+ void dropFrame(Packet *packet, const Ptr& header);
+ void dropRtsProtectedFrame(const Ptr& header);
virtual int getLongRetryLimit() { return longRetryLimit; }
virtual int getShortRetryLimit() { return shortRetryLimit; }
diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc
index 484f239cb09..b99e0a3339b 100644
--- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc
+++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc
@@ -98,6 +98,8 @@ AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const PtrgetIsAddbaResponseReceived() && header->getAMsduPresent() && !agreement->isAMsduPermitted())
+ throw cRuntimeError("A-MSDU is not permitted by both ADDBA flags");
if (agreement->getIsAddbaResponseReceived() && isBlockAckPolicyEligibleFrame(packet, header)) {
if (checkAgreementPolicy(header, agreement))
return AckPolicy::BLOCK_ACK;
@@ -116,7 +118,7 @@ bool OriginatorQosAckPolicy::isBlockAckPolicyEligibleFrame(Packet *packet, const
bool OriginatorQosAckPolicy::checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const
{
bool bufferFull = agreement->getBufferSize() == agreement->getNumSentBaPolicyFrames();
- bool aMsduOk = agreement->getIsAMsduSupported() || !header->getAMsduPresent();
+ bool aMsduOk = agreement->isAMsduPermitted() || !header->getAMsduPresent();
// TODO bool baPolicy = agreement->getIsDelayedBlockAckPolicySupported() || !frame->getAckPolicy();
return !bufferFull && aMsduOk && (header->getSequenceNumber() >= agreement->getStartingSequenceNumber()); // TODO && baPolicy
}
diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc
index ef82c225c78..76d15004acb 100644
--- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc
+++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc
@@ -9,11 +9,13 @@
#include
+#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h"
#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h"
#include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h"
#include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h"
#include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h"
#include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h"
+#include "inet/queueing/function/PacketFilterFunction.h"
namespace inet {
namespace ieee80211 {
@@ -33,9 +35,29 @@ void OriginatorQosMacDataService::initialize()
fragmentation = new Fragmentation();
}
-Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue)
+Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected)
{
auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue);
+ if (subframes) {
+ bool valid = !subframes->empty() && subframes->front() == selected;
+ std::set unique;
+ for (auto frame : *subframes) {
+ bool present = false;
+ for (int i = 0; i < pendingQueue->getNumPackets(); i++) {
+ if (pendingQueue->getPacket(i) == frame) {
+ present = true;
+ break;
+ }
+ }
+ valid &= present && unique.insert(frame).second &&
+ (!extractionEligible || extractionEligible(frame)) &&
+ (!aMsduPermitted || aMsduPermitted(frame));
+ }
+ if (!valid) {
+ delete subframes;
+ return nullptr;
+ }
+ }
if (subframes) {
std::vector sourceIds;
AccessCategory ac = AC_BE;
@@ -105,16 +127,21 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu
Enter_Method("extractFramesToTransmit");
if (expireFrames)
expireFrames();
- if (pendingQueue->isEmpty())
+ auto selected = peekEligibleFrame(pendingQueue);
+ if (selected == nullptr)
return nullptr;
else {
// if (msduRateLimiting)
// txRateLimitingIfNeeded();
Packet *packet = nullptr;
- if (aMsduAggregationPolicy)
- packet = aMsduAggregateIfNeeded(pendingQueue);
+ auto module = dynamic_cast(pendingQueue);
+ auto next = pendingQueue->canPullPacket(module ? module->gate("out") : nullptr);
+ if (selected == next && aMsduAggregationPolicy && (!aMsduPermitted || aMsduPermitted(selected)))
+ packet = aMsduAggregateIfNeeded(pendingQueue, selected);
if (!packet) {
- packet = pendingQueue->dequeuePacket();
+ queueing::CPacketFilterFunction filter([selected](const Packet *candidate) { return candidate == selected; });
+ packet = selected == next ? pendingQueue->dequeuePacket() :
+ pendingQueue->pullPacket(module ? module->gate("out") : nullptr, &filter);
take(packet);
}
// PS Defer Queueing
@@ -123,6 +150,8 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu
assignSequenceNumber(header);
packet->insertAtFront(header);
}
+ if (dynamicPtrCast(packet->peekAtFront()))
+ packet->addTagIfAbsent()->setAllocationIndex(nextDataAllocationIndex++);
// if (msduIntegrityAndProtection)
// frame = protectMsduIfNeeded(frame);
std::vector *fragments = nullptr;
@@ -143,6 +172,23 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu
}
}
+Packet *OriginatorQosMacDataService::peekEligibleFrame(queueing::IPacketQueue *queue) const
+{
+ if (queue->isEmpty())
+ return nullptr;
+ queueing::CPacketFilterFunction filter([this](const Packet *packet) { return !extractionEligible || extractionEligible(packet); });
+ auto module = dynamic_cast(queue);
+ auto packet = queue->canPullPacket(module ? module->gate("out") : nullptr);
+ if (packet && filter.matchesPacket(packet))
+ return packet;
+ return queue->canPullPacket(module ? module->gate("out") : nullptr, &filter);
+}
+
+SequenceNumberCyclic OriginatorQosMacDataService::getNextSequenceNumber(MacAddress peer, Tid tid) const
+{
+ return check_and_cast(sequenceNumberAssignment)->getNextSequenceNumber(peer, tid);
+}
+
OriginatorQosMacDataService::~OriginatorQosMacDataService()
{
delete aMsduAggregation;
diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h
index 9e377d36427..7928e2153bc 100644
--- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h
+++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h
@@ -41,16 +41,26 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p
IMpduAggregation *aMpduAggregation = nullptr;
EdcaTransmitLifetimeHandler *lifetimeHandler = nullptr; // Borrowed from EDCA.
std::function expireFrames;
+ std::function extractionEligible;
+ std::function aMsduPermitted;
+ int64_t nextDataAllocationIndex = 0;
protected:
virtual void initialize() override;
virtual void assignSequenceNumber(const Ptr& header);
virtual std::vector *fragmentIfNeeded(Packet *frame);
- virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue);
+ virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected);
virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments);
public:
+ void setEligibility(std::function eligible, std::function