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 permission) { + extractionEligible = eligible; + aMsduPermitted = permission; + } + Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const; + SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const; + bool hasAMsduAggregation() const { return aMsduAggregationPolicy != nullptr; } virtual ~OriginatorQosMacDataService(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler, std::function expiration) { lifetimeHandler = handler; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 76838835cce..e9918ee20f7 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -63,6 +63,7 @@ bool InProgressFrames::containsFrame(const Packet *packet) const bool InProgressFrames::isEligibleToTransmit(Packet *packet) { return (!lifetimeHandler || !lifetimeHandler->isExpired(packet)) && + (!transmissionEligible || transmissionEligible(packet)) && ackHandler->isEligibleToTransmit(packet->peekAtFront()); } diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index ca7175ee2d5..293aebfc3a3 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -32,6 +32,7 @@ class INET_API InProgressFrames : public SimpleModule EdcaTransmitLifetimeHandler *lifetimeHandler = nullptr; // Borrowed from EDCA; null for DCF. std::vector inProgressFrames; std::vector droppedFrames; + std::function transmissionEligible; protected: virtual void initialize(int stage) override; @@ -43,6 +44,8 @@ class INET_API InProgressFrames : public SimpleModule public: virtual ~InProgressFrames(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler) { lifetimeHandler = handler; } + void setTransmissionEligibility(std::function predicate) { transmissionEligible = predicate; } + bool hasEligibleInProgressFrames() { return hasEligibleFrameToTransmit(); } virtual std::string str() const override; virtual void forEachChild(cVisitor *v) override; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 95fac972e3a..e73a15720c1 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -57,6 +57,11 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data { Enter_Method("dataFrameReceived"); take(dataPacket); + if (blockAckAgreementHandler) { + auto agreement = blockAckAgreementHandler->getAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); + if (agreement) + agreement->recordReceived(dataHeader); + } // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; @@ -116,20 +121,17 @@ std::vector RecipientQosMacDataService::managementFrameReceived(Packet Enter_Method("managementFrameReceived"); take(mgmtPacket); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption - if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) + if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { + delete mgmtPacket; return std::vector(); + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting - if (dynamicPtrCast(mgmtHeader)) { - delete mgmtPacket; + if (mgmtPacket == nullptr) return std::vector(); - } - else - return std::vector({ mgmtPacket }); + return std::vector({ mgmtPacket }); } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) @@ -193,6 +195,49 @@ RecipientQosMacDataService::~RecipientQosMacDataService() delete blockAckReordering; } +std::vector RecipientQosMacDataService::retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) +{ + Enter_Method("retireAgreement"); + auto peer = agreement->getBlockAckRecord()->getOriginatorAddress(); + auto tid = agreement->getBlockAckRecord()->getTid(); + auto duplicate = check_and_cast(duplicateRemoval); + // IEEE Std 802.11-2024, 11.5.3.5: ordinary teardown retains the highest received sequence number. + if (!associationReset) + duplicate->setLastReceived(peer, tid, agreement->getHighestReceivedSequence(), agreement->getHighestReceivedFragment()); + std::vector discarded; + auto released = blockAckReordering->detachAgreement(peer, tid, !associationReset, discarded); + for (auto packet : discarded) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + std::vector complete; + for (const auto& unit : released) { + auto frame = defragment(unit.second); + if (!frame) + continue; + if (frame->peekAtFront()->getAMsduPresent()) { + emit(packetDeaggregatedSignal, frame); + auto subframes = aMsduDeaggregation->deaggregateFrame(frame); + complete.insert(complete.end(), subframes->begin(), subframes->end()); + delete subframes; + } + else + complete.push_back(frame); + } + return complete; +} + +void RecipientQosMacDataService::clearPeer(MacAddress peer) +{ + Enter_Method("clearPeer"); + blockAckReordering->clearPeer(peer); + check_and_cast(duplicateRemoval)->clearPeer(peer); + for (int tid = -1; tid < 16; tid++) + basicReassembly->purge(peer, tid, 0, 4095); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index e4b8ee28865..10b26ca843f 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -46,6 +46,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, virtual Packet *defragment(Packet *mgmtFragment); public: + virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) override; + virtual void clearPeer(MacAddress peer) override; virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; diff --git a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc index c0d2085cf3d..8f5f1340436 100644 --- a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc +++ b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc @@ -10,6 +10,12 @@ namespace inet { namespace ieee80211 { +SequenceNumberCyclic QoSSequenceNumberAssignment::getNextSequenceNumber(MacAddress peer, Tid tid) const +{ + auto it = lastSentSeqNums.find({peer, tid}); + return it == lastSentSeqNums.end() ? SequenceNumberCyclic(0) : it->second + 1; +} + QoSSequenceNumberAssignment::CacheType QoSSequenceNumberAssignment::getCacheType(const Ptr& header, bool incoming) { bool isTimePriorityFrame = false; // TODO diff --git a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h index 07928c860b5..81cb584ace8 100644 --- a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h +++ b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h @@ -36,6 +36,7 @@ class INET_API QoSSequenceNumberAssignment : public ISequenceNumberAssignment virtual CacheType getCacheType(const Ptr& header, bool incoming); public: + SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const; virtual void assignSequenceNumber(const Ptr& header) override; }; diff --git a/src/inet/queueing/function/PacketFilterFunction.h b/src/inet/queueing/function/PacketFilterFunction.h index 04634ac4968..b345b1e89f7 100644 --- a/src/inet/queueing/function/PacketFilterFunction.h +++ b/src/inet/queueing/function/PacketFilterFunction.h @@ -8,6 +8,8 @@ #ifndef __INET_PACKETFILTERFUNCTION_H #define __INET_PACKETFILTERFUNCTION_H +#include + #include "inet/queueing/contract/IPacketFilterFunction.h" namespace inet { @@ -18,10 +20,10 @@ typedef bool (*PacketFilterFunction)(const Packet *packet); class INET_API CPacketFilterFunction : public cObject, public virtual IPacketFilterFunction { protected: - PacketFilterFunction packetFilterFunction; + std::function packetFilterFunction; public: - CPacketFilterFunction(PacketFilterFunction packetFilterFunction) : packetFilterFunction(packetFilterFunction) {} + CPacketFilterFunction(std::function packetFilterFunction) : packetFilterFunction(packetFilterFunction) {} virtual bool matchesPacket(const Packet *packet) const override { return packetFilterFunction(packet); } }; diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 294db153bd8..78905809d2c 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -7,7 +7,7 @@ # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, dd36-1076/tplx;b703-3ef2/~tNl;e07d-2980/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, b80d-4ae8/tplx;72b7-86c2/~tNl;d385-62e0/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 5fbb-9fd9/tplx;c0a4-97f6/~tNl;2373-df01/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 2f2d-73b1/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 2c80-6844/tplx;4e9e-cf86/~tNl;ea3d-e802/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -658,7 +658,7 @@ /examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, d929-5913/tplx;f602-4424/~tNl;f2fb-4d98/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 0473-f9d2/tplx;e92c-7352/~tNl;317d-aa4f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, e07a-4c5f/tplx;9f6c-7889/~tNl;29ea-169c/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, fbab-b84e/tplx;47cb-b9e5/~tNl;c4f3-fa00/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, f8a4-9474/tplx;1cba-cccc/~tNl;538e-67df/~tND;08b5-d005/tyf, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 572d1330c90..836d42b5183 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -199,9 +199,9 @@ /showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;7b7c-2bfb/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;d680-b8ad/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 1082-899c/tplx;0068-9ae9/~tNl;e0b3-3193/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 2235-516d/tplx;7b51-e67c/~tNl;9a3f-afc7/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 8bfb-6ac9/tplx;5647-2cd1/~tNl;481a-ec9d/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, b828-6bd6/tplx;026f-2c6d/~tNl;2cd4-4795/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 021a-d29a/tplx;1597-ec6a/~tNl;feff-7e88/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 5b9e-801f/tplx;8731-2504/~tNl;9724-d1ce/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;fc60-7eb7/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;6f3a-074a/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -267,7 +267,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 8df2-40a3/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, c99f-354d/tplx;8e10-a3f0/~tNl;ac4b-3ba1/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 4228-9743/tplx;8610-abcd/~tNl;68c2-19f0/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, f509-65f1/tplx;6b3a-0d65/~tNl;c474-7174/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 6150-7a8b/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -341,5 +341,5 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb5-d354/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, 4ffd-4fbf/tplx;3a89-2021/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, f58d-d2fd/tplx;38f7-f642/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 diff --git a/tests/module/Ieee80211BlockAckActionDelivery_1.test b/tests/module/Ieee80211BlockAckActionDelivery_1.test new file mode 100644 index 00000000000..b2ac3745b86 --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDelivery_1.test @@ -0,0 +1,70 @@ +%description: +Check complete Block Ack Action delivery through the production recipient data service. +An original Action reaches HCF. Its MAC retry does not reach the agreement handler. +A new Action with another sequence number remains eligible for dispatch. +Two management fragments produce one complete Action after the final fragment. + +%file: ActionDelivery.cc +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" + +namespace inet::ieee80211 { +class ActionDeliveryTest : public cSimpleModule { + protected: + void initialize() override { + scheduleAt(SIMTIME_ZERO, new cMessage("test")); + } + void handleMessage(cMessage *message) override { + delete message; + auto service = check_and_cast(getModuleByPath("^.recipient")); + for (int index = 0; index < 6; index++) { + auto header = makeShared(); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setReceiverAddress(MacAddress("02:00:00:00:00:01")); + header->setSequenceNumber(SequenceNumberCyclic(index < 2 ? 10 : index == 2 ? 11 : 12)); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + header->setDialogToken(37); + header->setTid(5); + header->setBufferSize(8); + header->setBlockAckPolicy(true); + header->setRetry(index == 1 || index == 5); + header->setMoreFragments(index == 3); + header->setFragmentNumber(index >= 4 ? 1 : 0); + auto packet = new Packet("action", header); + packet->insertAtBack(makeShared(B(2))); + packet->insertAtBack(makeShared()); + auto frames = service->managementFrameReceived(packet, header); + ASSERT(frames.size() == (index == 0 || index == 2 || index == 4 ? 1 : 0)); + for (auto frame : frames) { + ASSERT(frame->peekAtFront() != nullptr); + take(frame); + delete frame; + } + } + std::cout << "Block Ack Action delivery assertions passed.\n"; + } +}; +Define_Module(ActionDeliveryTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +simple ActionDeliveryTest { parameters: @class(::inet::ieee80211::ActionDeliveryTest); } +network ActionDeliveryNetwork { + submodules: + recipient: RecipientQosMacDataService; + test: ActionDeliveryTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDeliveryNetwork +ned-path = .;../../../../src +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Block Ack Action delivery assertions passed. diff --git a/tests/module/Ieee80211BlockAckActionDispatch_1.test b/tests/module/Ieee80211BlockAckActionDispatch_1.test new file mode 100644 index 00000000000..1ec9db351e9 --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDispatch_1.test @@ -0,0 +1,178 @@ +%description: +Check duplicate Block Ack Actions through initialized production HCF. +The test observes response submission without a wireless transmission. +HCF must acknowledge a MAC retry without a second agreement dispatch. + +The deadline check derives 5 ms from an agreement created at 1 ms with a 4 ms timeout. + +%extraargs: -r 0..2 + +%file: ActionDispatch.cc +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ActionDispatchHcf : public Hcf { + public: + int dispatches = 0; + int acknowledgments = 0; + int responses = 0; + int delivered = 0; + Ptr response; + void checkAgreementDeadline() { + Enter_Method("checkAgreementDeadline"); + receiveRequest(false); + auto expectedDeadline = SimTime(5, SIMTIME_MS); + ASSERT(recipientBlockAckAgreementHandler->computeEarliestExpirationTime() == expectedDeadline); + scheduleInactivityTimer(); + ASSERT(inactivityTimer->getArrivalTime() == expectedDeadline); + cancelEvent(inactivityTimer); + } + void receiveRequest(bool retry) { + Enter_Method("receiveRequest"); + auto header = makeShared(); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setReceiverAddress(mac->getAddress()); + header->setSequenceNumber(SequenceNumberCyclic(10)); + header->setRetry(retry); + header->setTid(5); + header->setDialogToken(37); + header->setBlockAckPolicy(true); + header->setBufferSize(8); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("request", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode( + modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + } + void checkNegotiatedWindow() { + Enter_Method("checkNegotiatedWindow"); + receiveRequest(false); + ASSERT(response != nullptr && response->getBufferSize() == 8); + auto agreement = recipientBlockAckAgreementHandler->getAgreement(5, MacAddress("02:00:00:00:00:02")); + ASSERT(agreement != nullptr && agreement->getBufferSize() == 8); + auto bar = makeShared(); + bar->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + bar->setReceiverAddress(mac->getAddress()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(104)); + bar->setFragmentNumber(0); + auto request = new Packet("bar-before-data", bar); + request->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(request, bar); + ASSERT(delivered == 0); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(data->getTransmitterAddress()); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(104)); + data->setAckPolicy(BLOCK_ACK); + auto packet = new Packet("first-data", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, data); + ASSERT(delivered == 1); + } + protected: + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered++; + take(frame); + delete frame; + } + } + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + dispatches++; + Hcf::recipientProcessReceivedManagementFrame(header); + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + acknowledgments++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + responses++; + response = dynamicPtrCast(header); + delete packet; + } +}; +Define_Module(ActionDispatchHcf); +class ActionDispatchTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + if (par("scenario").intValue() == 1) + hcf->checkAgreementDeadline(); + else if (par("scenario").intValue() == 2) + hcf->checkNegotiatedWindow(); + else { + hcf->receiveRequest(false); + hcf->receiveRequest(true); + ASSERT(hcf->acknowledgments == 2); + ASSERT(hcf->dispatches == 1); + ASSERT(hcf->responses == 1); + ASSERT(hcf->response != nullptr && hcf->response->getStatusCode() == SC_SUCCESSFUL); + ASSERT(hcf->response->getDialogToken() == 37); + ASSERT(hcf->response->getBufferSize() == 64); + ASSERT(hcf->response->getBlockAckTimeoutValue() == SimTime(4, SIMTIME_MS)); + } + std::cout << "Block Ack Action dispatch assertions passed.\n"; + } +}; +Define_Module(ActionDispatchTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ActionDispatchHcf extends Hcf { parameters: @class(::inet::ieee80211::ActionDispatchHcf); } +simple ActionDispatchTest { parameters: int scenario; @class(::inet::ieee80211::ActionDispatchTest); } +network ActionDispatchNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ActionDispatchTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDispatchNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +*.test.scenario = ${scenario=0,1,2} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ActionDispatchHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.blockAckTimeoutValue = 4ms +**.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.maximumAllowedBufferSize = ${scenario} == 2 ? 8 : 64 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack Action dispatch assertions passed. diff --git a/tests/module/Ieee80211BlockAckActionDisposal_1.test b/tests/module/Ieee80211BlockAckActionDisposal_1.test new file mode 100644 index 00000000000..3704c34636c --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDisposal_1.test @@ -0,0 +1,470 @@ +%description: +Check owned Block Ack Actions through actual pending-queue overflow. Request disposal before transmission cancels initial setup without teardown. Successful response disposal preserves its recipient agreement. Originator DELBA disposal drops its deferred batch and leaves recovery required. + +Later DATA starts another recovery DELBA. Recipient DELBA disposal preserves the opposite role. Later VO queue eviction must release deferred BE DATA without another BE arrival. An isolated cleanup case supplies another peer's retry through the production recovery API. Reset must clear a retry from an actual failed RTS before the protected Action transmits. + +The fixture supplies agreement records and does not transmit frames through a peer radio. + +%extraargs: -r 0..6,8,10 + +%file: ActionDisposal.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/queueing/queue/PacketQueue.h" + +namespace inet::ieee80211 { +class DisposalQueue : public queueing::PacketQueue { + public: + void setCapacity(int capacity) { packetCapacity = capacity; } +}; +Define_Module(DisposalQueue); +class DisposalOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid = 5) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(100), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } + void prepare(const Ptr& request) { createAgreement(request); } +}; +class DisposalRecipient : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; +class ActionDisposalHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + int queueDrops = 0; + int requestDrops = 0; + int responseDrops = 0; + int delbaDrops = 0; + int delbaSubmissions = 0; + int attempts = 0; + int beAdmissions = 0; + int beAttempts = 0; + int requestAttempts = 0; + simtime_t firstBeAttempt = SIMTIME_MAX; + simtime_t beDeadline = SIMTIME_MAX; + Ptr rtsChargedHeader; + int selectedScenario = -1; + void run(int scenario) { + Enter_Method("run"); + selectedScenario = scenario; + delete originatorBlockAckAgreementHandler; + delete recipientBlockAckAgreementHandler; + auto originator = new DisposalOriginator(); + auto recipient = new DisposalRecipient(); + originatorBlockAckAgreementHandler = originator; + recipientBlockAckAgreementHandler = recipient; + auto queue = check_and_cast(edca->getEdcaf(AC_VO)->getPendingQueue()); + if (scenario == 9 || scenario == 10) { + queue->setCapacity(100); + queuePendingBeRequest(); + return; + } + if (scenario == 8) { + queue->setCapacity(100); + originator->accept(peer, 0); + beginTeardown(peer, 0, true, 37); + queueData("deferred-be-after-delba", 0); + ASSERT(!check_and_cast(edca->getEdcaf(AC_BE)->getSubmodule("contention"))->isContentionInProgress()); + scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verifyBeResume")); + return; + } + if (scenario == 6) { + queue->setCapacity(1); + queueVoice("voice-blocker", B(1000)); + return; + } + if (scenario == 7) { + queue->setCapacity(100); + auto edcaf = edca->getEdcaf(AC_VO); + auto recovery = edcaf->getMgmtAndNonQoSRecoveryProcedure(); + auto other = makeShared(); + other->setReceiverAddress(MacAddress("02:00:00:00:00:03")); + other->setSequenceNumber(SequenceNumberCyclic(0)); + auto otherPacket = new Packet("other-peer-retry", other); + recovery->dataOrMgmtFrameTransmissionFailed(otherPacket, other, edcaf->getStationRetryCounters()); + ASSERT(recovery->getRetryCount(otherPacket, other) == 1); + queuePendingBeRequest(); + auto selected = edcaf->getInProgressFrames()->getFrameToTransmit(); + ASSERT(dynamicPtrCast(selected->peekAtFront()) != nullptr); + ASSERT(edcaf->getInProgressFrames()->getLength() == 1); + auto request = edcaf->getInProgressFrames()->getFrames(0)->peekAtFront(); + ASSERT(request->getSequenceNumber() == other->getSequenceNumber()); + ASSERT(request->getFragmentNumber() == other->getFragmentNumber()); + ASSERT(attempts == 0); + throw cRuntimeError("Association reset requires the next commit"); + ASSERT(recovery->getRetryCount(otherPacket, other) == 1); + ASSERT(ownedActions.empty()); + delete otherPacket; + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + queue->setCapacity(scenario == 4 ? 1 : scenario == 5 ? 100 : 0); + handlingAgreementAction = true; + if (scenario == 0 || scenario == 4) { + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + originator->prepare(request); + queueData("deferred"); + processMgmtFrame(new Packet("request", request), request); + if (scenario == 4) { + ASSERT(queue->getNumPackets() == 1); + auto filler = makeShared(); + filler->setReceiverAddress(peer); + filler->setType(ST_PROBEREQUEST); + processUpperFrame(new Packet("filler", filler), filler); + } + ASSERT(requestDrops == 1); + ASSERT(originator->getAgreement(peer, 5) == nullptr); + ASSERT(originator->getProcedures().count({peer, 5}) == 0); + ASSERT(delbaSubmissions == 0); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 1); + } + else if (scenario == 1) { + originator->accept(peer); + auto request = makeShared(); + request->setTransmitterAddress(peer); + request->setReceiverAddress(mac->getAddress()); + request->setTid(5); + request->setDialogToken(37); + request->setSequenceNumber(SequenceNumberCyclic(10)); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("incoming-request", request); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, request); + ASSERT(responseDrops == 1); + ASSERT(recipient->getAgreement(5, peer) != nullptr); + ASSERT(originator->getAgreement(peer, 5) != nullptr); + } + else { + originator->accept(peer); + recipient->accept(peer); + if (scenario == 2 || scenario == 5) { + queueData("batch-first"); + queueData("batch-second"); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 2); + beginTeardown(peer, 5, true, 37); + if (scenario == 5) { + handlingAgreementAction = false; + updateContention(); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("delbaFailure")); + return; + } + ASSERT(originator->getAgreement(peer, 5) == nullptr); + ASSERT(recipient->getAgreement(5, peer) != nullptr); + ASSERT(originator->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::RECOVERY_NEEDED); + ASSERT(!hasQueuedData(peer, 5)); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(delbaDrops == 1); + ASSERT(delbaSubmissions == 1); + queue->setCapacity(100); + queueData("later"); + ASSERT(delbaSubmissions == 2); + ASSERT(originator->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING); + ASSERT(hasQueuedData(peer, 5)); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 1); + } + else { + beginTeardown(peer, 5, false, 37); + ASSERT(recipient->getAgreement(5, peer) == nullptr); + ASSERT(originator->getAgreement(peer, 5) != nullptr); + ASSERT(!recipient->isTeardownPending(peer, 5)); + ASSERT(delbaDrops == 1); + } + } + ASSERT(attempts == 0); + ASSERT(queueDrops == 1); + if (scenario != 2) + ASSERT(ownedActions.empty()); + handlingAgreementAction = false; + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + protected: + void handleMessage(cMessage *message) override { + if (message->isName("verifyRtsTerminal")) { + delete message; + ASSERT(rtsChargedHeader != nullptr); + ASSERT(attempts == 1); + ASSERT(requestAttempts == 0); + ASSERT(queueDrops == 0); + ASSERT(edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure()->getShortRetryCount(nullptr, rtsChargedHeader) == 0); + ASSERT(ownedActions.empty()); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 0) == nullptr); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({peer, 0}) == 0); + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + if (message->isName("resetAfterRtsFailure")) { + delete message; + auto recovery = edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure(); + ASSERT(rtsChargedHeader != nullptr); + ASSERT(recovery->getShortRetryCount(nullptr, rtsChargedHeader) == 1); + ASSERT(requestAttempts == 0); + throw cRuntimeError("Association reset requires the next commit"); + ASSERT(recovery->getShortRetryCount(nullptr, rtsChargedHeader) == 0); + ASSERT(ownedActions.empty()); + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + if (message->isName("delbaAck")) { + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("delba-ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({peer, 0}) == 0); + return; + } + if (message->isName("queueDeferredBe")) { + delete message; + ASSERT(tx->hasTransmission()); + auto contention = check_and_cast(edca->getEdcaf(AC_BE)->getSubmodule("contention")); + ASSERT(!contention->isContentionInProgress()); + queuePendingBeRequest(); + ASSERT(!contention->isContentionInProgress()); + ASSERT(edca->getEdcaf(AC_VO)->getPendingQueue()->getNumPackets() == 1); + ASSERT(requestAttempts == 0); + scheduleAfter(SimTime(100, SIMTIME_US), new cMessage("evictPendingRequest")); + return; + } + if (message->isName("evictPendingRequest")) { + delete message; + auto contention = check_and_cast(edca->getEdcaf(AC_BE)->getSubmodule("contention")); + ASSERT(!contention->isContentionInProgress()); + ASSERT(tx->hasTransmission()); + queueVoice("voice-evictor", B(10)); + ASSERT(requestDrops == 1); + ASSERT(requestAttempts == 0); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 0) == nullptr); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({peer, 0}) == 0); + ASSERT(hasQueuedData(peer, 0)); + scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verifyBeResume")); + return; + } + if (message->isName("verifyBeResume")) { + delete message; + ASSERT(beAdmissions == 1); + ASSERT(beAttempts >= 1); + ASSERT(firstBeAttempt < beDeadline); + ASSERT(simTime() < beDeadline); + ASSERT(requestAttempts == 0); + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + if (!message->isName("delbaFailure")) { + Hcf::handleMessage(message); + return; + } + delete message; + ASSERT(attempts == 1); + ASSERT(queueDrops == 0); + ASSERT(delbaSubmissions == 1); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 5) == nullptr); + ASSERT(recipientBlockAckAgreementHandler->getAgreement(5, peer) != nullptr); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::RECOVERY_NEEDED); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(!hasQueuedData(peer, 5)); + ASSERT(ownedActions.empty()); + handlingAgreementAction = true; + queueData("later-after-retry-limit"); + ASSERT(delbaSubmissions == 2); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 1); + handlingAgreementAction = false; + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + void queueVoice(const char *name, B payload) { + auto header = makeShared(); + header->setType(ST_PROBEREQUEST); + header->setReceiverAddress(MacAddress("02:00:00:00:00:03")); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(payload)); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + void queuePendingBeRequest() { + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(0); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + check_and_cast(originatorBlockAckAgreementHandler)->prepare(request); + queueData("deferred-be", 0); + processMgmtFrame(new Packet("pending-be-request", request), request); + } + void queueData(const char *name, Tid tid = 5) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(mac->getAddress()); + header->setReceiverAddress(peer); + header->setAddress3(peer); + header->setTid(tid); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + if (tid == 0) { + beAdmissions++; + beDeadline = edca->getLifetimeHandler()->getDeadline(packet); + } + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == packetDroppedSignal && dynamic_cast(source) != nullptr) { + queueDrops++; + auto packet = check_and_cast(object); + auto header = packet->peekAtFront(); + requestDrops += dynamicPtrCast(header) != nullptr; + responseDrops += dynamicPtrCast(header) != nullptr; + delbaDrops += dynamicPtrCast(header) != nullptr; + } + Hcf::receiveSignal(source, signal, object, details); + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbaSubmissions++; + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + attempts++; + auto header = packet->peekAtFront(); + auto data = dynamicPtrCast(header); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && packet->isName("voice-blocker")) + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("queueDeferredBe")); + if (accepted && data && data->getTid() == 0) { + beAttempts++; + firstBeAttempt = std::min(firstBeAttempt, simTime()); + } + if (accepted && dynamicPtrCast(header)) + requestAttempts++; + return accepted; + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + bool delba = dynamicPtrCast(header) != nullptr; + Hcf::transmissionComplete(packet, header); + if (delba && beAdmissions == 1) + scheduleAfter(SimTime(60, SIMTIME_US), new cMessage("delbaAck")); + } + void originatorProcessRtsProtectionFailed(Packet *packet) override { + Enter_Method("originatorProcessRtsProtectionFailed"); + auto request = dynamicPtrCast(packet->peekAtFront()); + Hcf::originatorProcessRtsProtectionFailed(packet); + if (request && selectedScenario == 10) { + rtsChargedHeader = request; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("verifyRtsTerminal")); + return; + } + if (request && !rtsChargedHeader) { + ASSERT(requestAttempts == 0); + ASSERT(ownedActions.count(packet->getId()) == 1); + ASSERT(!ownedActions.at(packet->getId()).attempted); + ASSERT(edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure()->getShortRetryCount(packet, request) == 1); + rtsChargedHeader = request; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("resetAfterRtsFailure")); + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { delete packet; } + void sendUp(const std::vector& packets) override { + for (auto packet : packets) { + take(packet); + delete packet; + } + } +}; +Define_Module(ActionDisposalHcf); +class ActionDisposalTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->run(par("scenario")); + } +}; +Define_Module(ActionDisposalTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.queue.PendingQueue; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ActionDisposalHcf extends Hcf { parameters: @class(::inet::ieee80211::ActionDisposalHcf); } +simple DisposalQueue extends PendingQueue { parameters: @class(::inet::ieee80211::DisposalQueue); } +simple ActionDisposalTest { parameters: int scenario; @class(::inet::ieee80211::ActionDisposalTest); } +network ActionDisposalNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ActionDisposalTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDisposalNetwork +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +*.test.scenario = ${scenario=0..10} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ActionDisposalHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.mgmtAndNonQoSRecoveryProcedure.shortRetryLimit = ${scenario} == 9 ? 3 : 1 +**.mgmtAndNonQoSRecoveryProcedure.longRetryLimit = 1 +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = (${scenario} == 9 || ${scenario} == 10) ? 0B : 2346B +**.wlan[*].mac.hcf.rtsPolicy.ctsTimeout = 1ms +**.wlan[*].mac.hcf.edca.edcaf[*].pendingQueue.typename = "DisposalQueue" +**.wlan[*].mac.hcf.edca.edcaf[*].pendingQueue.dropperClass = "inet::queueing::PacketAtCollectionBeginDropper" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack Action disposal assertions passed. diff --git a/tests/module/Ieee80211BlockAckAdmission_1.test b/tests/module/Ieee80211BlockAckAdmission_1.test new file mode 100644 index 00000000000..ef1c114ebfa --- /dev/null +++ b/tests/module/Ieee80211BlockAckAdmission_1.test @@ -0,0 +1,132 @@ +%description: +Check isolated profile admission through initialized production policies. +Basic permits legacy capability state. Compressed requires local and peer HT eligibility. +The fixture leaves production HCF on Basic and creates no Compressed agreement. + +%file: Admission.cc +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" + +namespace inet::ieee80211 { +class AdmissionTest : public cSimpleModule { + protected: + void checkResponse(IOriginatorBlockAckAgreementPolicy *policy) { + const MacAddress peer("02:00:00:00:00:02"); + OriginatorBlockAckAgreement agreement(peer, 5, SequenceNumberCyclic(100), 64, true, false); + agreement.setDialogToken(37); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setTid(5); + response->setDialogToken(37); + response->setStatusCode(SC_SUCCESSFUL); + response->setBlockAckPolicy(true); + response->setBufferSize(8); + ASSERT(policy->isAddbaReqAccepted(response, &agreement)); + ASSERT(!policy->isAddbaReqAccepted(response, nullptr)); + response->setDialogToken(0); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setDialogToken(38); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setDialogToken(37); + response->setTid(4); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setTid(5); + response->setTransmitterAddress(MacAddress("02:00:00:00:00:03")); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setTransmitterAddress(peer); + response->setStatusCode(SC_UNSPECIFIED); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setStatusCode(SC_SUCCESSFUL); + response->setBlockAckPolicy(false); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setBlockAckPolicy(true); + response->setBufferSize(0); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setBufferSize(8); + agreement.setIsAddbaResponseReceived(true); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + } + + void checkRequest(IRecipientBlockAckAgreementPolicy *policy) { + auto request = makeShared(); + request->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + request->setTid(5); + request->setDialogToken(37); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + request->setBlockAckPolicy(true); + for (int size : {0, 8, 64, 128}) { + request->setBufferSize(size); + ASSERT(policy->isAddbaReqAccepted(request)); + } + request->setBlockAckPolicy(false); + ASSERT(!policy->isAddbaReqAccepted(request)); + request->setBlockAckPolicy(true); + request->setDialogToken(0); + ASSERT(!policy->isAddbaReqAccepted(request)); + request->setDialogToken(37); + request->setTransmitterAddress(MacAddress::BROADCAST_ADDRESS); + ASSERT(!policy->isAddbaReqAccepted(request)); + } + + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + auto originator = check_and_cast( + getModuleByPath("^.host.wlan[0].mac.hcf.originatorBlockAckAgreementPolicy")); + auto recipient = check_and_cast( + getModuleByPath("^.host.wlan[0].mac.hcf.recipientBlockAckAgreementPolicy")); + checkResponse(originator); + checkRequest(recipient); + for (bool localHt : {false, true}) { + for (bool peerHt : {false, true}) { + ASSERT(originator->isProfileAllowed(BlockAckVariant::BASIC, localHt, peerHt)); + ASSERT(recipient->isProfileAllowed(BlockAckVariant::BASIC, localHt, peerHt)); + ASSERT(originator->isProfileAllowed(BlockAckVariant::COMPRESSED, localHt, peerHt) == (localHt && peerHt)); + ASSERT(recipient->isProfileAllowed(BlockAckVariant::COMPRESSED, localHt, peerHt) == (localHt && peerHt)); + } + } + std::cout << "Block Ack admission assertions passed.\n"; + } +}; +Define_Module(AdmissionTest); +} + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple AdmissionTest { parameters: @class(::inet::ieee80211::AdmissionTest); } +network AdmissionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: AdmissionTest; +} + +%inifile: omnetpp.ini +[General] +network = AdmissionNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack admission assertions passed. diff --git a/tests/module/Ieee80211BlockAckDataSelection_1.test b/tests/module/Ieee80211BlockAckDataSelection_1.test new file mode 100644 index 00000000000..1282837f8ca --- /dev/null +++ b/tests/module/Ieee80211BlockAckDataSelection_1.test @@ -0,0 +1,321 @@ +%description: +Check DATA extraction through the production QoS data service. +A compound queue uses reversed service order and a multicast/unicast classifier. +The service must extract unicast DATA individually without a multicast aggregate. +The multicast sources must retain their identities, sequence numbers, and deadlines. +Other cases use a compound queue whose provider supports filtered queries but rejects filtered pulls. +Ordinary service and filtered queries must work. A refused pull must preserve queued DATA. + +%extraargs: -r 0..3 + +%file: DataSelection.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/queueing/base/PacketQueueBase.h" +#include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/common/PacketEventTag.h" +#include "inet/common/TimeTag.h" + +using namespace inet; +using namespace inet::ieee80211; + +class LegacySelectionQueue : public queueing::PacketQueueBase { + cPacketQueue frames; + public: + int getMaxNumPackets() const override { return -1; } + int getNumPackets() const override { return frames.getLength(); } + b getMaxTotalLength() const override { return b(-1); } + b getTotalLength() const override { return b(frames.getBitLength()); } + Packet *getPacket(int index) const override { return check_and_cast(frames.get(index)); } + bool isEmpty() const override { return frames.isEmpty(); } + void removePacket(Packet *frame) override { frames.remove(frame); } + void removeAllPackets() override { frames.clear(); } + bool supportsPacketPushing(const cGate *) const override { return true; } + bool supportsPacketPulling(const cGate *) const override { return true; } + bool canPushPacket(Packet *, const cGate *) const override { return true; } + void pushPacket(Packet *frame, const cGate *) override { frames.insert(frame); } + Packet *canPullPacket(const cGate *) const override { return isEmpty() ? nullptr : getPacket(0); } + Packet *canPullPacket(const cGate *, const queueing::IPacketFilterFunction *filter) const override { + for (cQueue::Iterator it(frames); !it.end(); it++) { + auto frame = check_and_cast(*it); + if (filter->matchesPacket(frame)) + return frame; + } + return nullptr; + } + Packet *pullPacket(const cGate *) override { return check_and_cast(frames.pop()); } + void enqueuePacket(Packet *frame) override { frames.insert(frame); } + Packet *dequeuePacket() override { return check_and_cast(frames.pop()); } +}; +Define_Module(LegacySelectionQueue); + +class CompatibilityCompoundQueue : public queueing::CompoundPacketQueueBase { + public: + bool unrelatedFailure = false; + Packet *canPullPacket(const cGate *gate, const queueing::IPacketFilterFunction *filter) const override { + if (unrelatedFailure) + throw cRuntimeError("Unrelated fixture query failure"); + return CompoundPacketQueueBase::canPullPacket(gate, filter); + } +}; +Define_Module(CompatibilityCompoundQueue); + +class DataSelectionHcf : public Hcf { + public: + bool queued(MacAddress peer, Tid tid) const { return hasQueuedData(peer, tid); } +}; +Define_Module(DataSelectionHcf); + +class DataSelectionTest : public cSimpleModule, public cListener { + int pulls = 0; + int removals = 0; + int aggregates = 0; + protected: + void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { + if (signal == packetPulledSignal) pulls++; + if (signal == packetRemovedSignal) removals++; + if (signal == IOriginatorMacDataService::packetAggregatedSignal) aggregates++; + } + Packet *makeFrame(const char *name, MacAddress receiver) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(0); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(receiver); + auto frame = new Packet(name, header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + auto timeTag = frame->addRegionTag(); + timeTag->setPacketTotalTimesArraySize(1); + timeTag->setPacketTotalTimes(0, SIMTIME_ZERO); + frame->addRegionTag(); + return frame; + } + Packet *accept(Hcf *hcf, const char *name, MacAddress receiver) { + auto frame = makeFrame(name, receiver); + hcf->processUpperFrame(frame, frame->peekAtFront()); + return frame; + } + void activity() override { + auto mac = getModuleByPath("^.host.wlan[0].mac"); + auto hcf = check_and_cast(mac->getSubmodule("hcf")); + auto edca = check_and_cast(hcf->getSubmodule("edca")); + auto queue = edca->getEdcaf(AC_BE)->getPendingQueue(); + auto lifetime = edca->getLifetimeHandler(); + auto service = check_and_cast(hcf->getSubmodule("originatorMacDataService")); + auto queueModule = check_and_cast(queue); + check_and_cast(mac->getSubmodule("rx"))->setContentionBlocked(true); + int scenario = par("scenario"); + if (scenario != 0) { + MacAddress firstPeer("02:00:00:00:00:02"); + MacAddress secondPeer("02:00:00:00:00:03"); + Packet *head; + Packet *tail; + if (scenario == 1) { + head = accept(hcf, "ordinary-head", firstPeer); + tail = accept(hcf, "ordinary-tail", secondPeer); + ASSERT(hcf->queued(firstPeer, 0)); + ASSERT(hcf->queued(secondPeer, 0)); + ASSERT(!hcf->queued(firstPeer, 5)); + service->setEligibility([](const Packet *) { return true; }, [](Packet *) { return false; }); + ASSERT(service->peekEligibleFrame(queue) == head); + auto ordinary = service->extractFramesToTransmit(queue); + ASSERT(ordinary != nullptr && ordinary->size() == 1 && ordinary->front() == head); + ASSERT(queue->getNumPackets() == 1 && queue->getPacket(0) == tail); + ASSERT(head->getOwner() == service); + ASSERT(head->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(0)); + lifetime->forgetFrame(head->getId()); + take(head); + delete head; + delete ordinary; + auto composite = check_and_cast(queue); + composite->unrelatedFailure = true; + bool propagated = false; + try { hcf->queued(firstPeer, 0); } + catch (const cRuntimeError& error) { + propagated = std::string(error.what()).find("Unrelated fixture query failure") != std::string::npos; + } + ASSERT(propagated); + } + else { + head = makeFrame("deferred-head", firstPeer); + tail = makeFrame("eligible-tail", secondPeer); + queue->enqueuePacket(head); + queue->enqueuePacket(tail); + lifetime->registerFrame(head, AC_BE); + lifetime->registerFrame(tail, AC_BE); + service->setEligibility([scenario, tail](const Packet *frame) { return scenario == 3 && frame == tail; }, + [](Packet *) { return false; }); + if (scenario == 2) { + ASSERT(service->peekEligibleFrame(queue) == nullptr); + ASSERT(service->extractFramesToTransmit(queue) == nullptr); + ASSERT(hcf->queued(secondPeer, 0)); + } + else { + ASSERT(hcf->queued(secondPeer, 0)); + bool refused = false; + try { service->extractFramesToTransmit(queue); } + catch (const cRuntimeError& error) { + refused = std::string(error.what()).find("Invalid operation") != std::string::npos; + } + ASSERT(refused); + } + ASSERT(queue->getNumPackets() == 2 && queue->getPacket(0) == head && queue->getPacket(1) == tail); + ASSERT(head->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime->getDeadline(head) == SimTime(512, SIMTIME_MS)); + } + ASSERT(tail->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime->getDeadline(tail) == SimTime(512, SIMTIME_MS)); + std::cout << "Compound compatibility scenario " << scenario << " passed.\n"; + endSimulation(); + } + queueModule->subscribe(packetPulledSignal, this); + queueModule->subscribe(packetRemovedSignal, this); + service->subscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + auto first = accept(hcf, "multicast-first", MacAddress("01:00:5e:00:00:01")); + auto second = accept(hcf, "multicast-second", MacAddress("01:00:5e:00:00:01")); + auto selected = accept(hcf, "selected-unicast", MacAddress("02:00:00:00:00:02")); + auto deadline = lifetime->getDeadline(first); + ASSERT(deadline == SimTime(512, SIMTIME_MS)); + ASSERT(queue->getPacket(0) == first); + ASSERT(service->peekEligibleFrame(queue) == selected); + ASSERT(service->getNextSequenceNumber(MacAddress("02:00:00:00:00:02"), 0) == SequenceNumberCyclic(0)); + wait(SimTime(100, SIMTIME_MS)); + auto extracted = service->extractFramesToTransmit(queue); + ASSERT(extracted != nullptr && extracted->size() == 1); + ASSERT(extracted->front() == selected); + ASSERT(selected->getOwner() == service); + auto header = selected->peekAtFront(); + ASSERT(!header->getAMsduPresent()); + ASSERT(header->getSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(queue->getNumPackets() == 2); + ASSERT(queue->getPacket(0) == first && queue->getPacket(1) == second); + for (auto source : {first, second}) { + ASSERT(source->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime->getDeadline(source) == deadline); + ASSERT(lifetime->findFrame(source->getId()) == source); + ASSERT(source->findRegionTag()->getPacketTotalTimes(0) == SIMTIME_ZERO); + ASSERT(source->findRegionTag()->getPacketEventsArraySize() == 0); + } + auto times = selected->getAllRegionTags(); + ASSERT(times.size() == 1); + ASSERT(times.front().getTag()->getPacketTotalTimes(0) == SimTime(100, SIMTIME_MS)); + auto eventRegions = selected->getAllRegionTags(); + ASSERT(eventRegions.size() == 1); + auto events = eventRegions.front().getTag(); + ASSERT(events->getPacketEventsArraySize() == 1); + ASSERT(events->getPacketEvents(0)->getKind() == PEK_QUEUED); + ASSERT(events->getPacketEvents(0)->getPacketDuration() == SimTime(100, SIMTIME_MS)); + ASSERT(lifetime->getDeadline(selected) == deadline); + ASSERT(lifetime->getNumFrames() == 3); + ASSERT(pulls == 3); + ASSERT(removals == 0 && aggregates == 0); + lifetime->forgetFrame(selected->getId()); + take(selected); + delete selected; + delete extracted; + queueModule->unsubscribe(packetPulledSignal, this); + queueModule->unsubscribe(packetRemovedSignal, this); + service->unsubscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + auto legacy = check_and_cast(getModuleByPath("^.legacy")); + auto head = first->dup(); + auto tail = first->dup(); + legacy->enqueuePacket(head); + legacy->enqueuePacket(tail); + service->setEligibility([](const Packet *) { return false; }, [](Packet *) { return false; }); + ASSERT(service->extractFramesToTransmit(legacy) == nullptr); + ASSERT(legacy->getNumPackets() == 2 && legacy->getPacket(0) == head); + service->setEligibility([tail](const Packet *frame) { return frame == tail; }, [](Packet *) { return false; }); + bool refused = false; + try { service->extractFramesToTransmit(legacy); } + catch (const cRuntimeError& error) { + refused = std::string(error.what()).find("Invalid operation") != std::string::npos; + } + ASSERT(refused); + ASSERT(legacy->getNumPackets() == 2 && legacy->getPacket(0) == head && legacy->getPacket(1) == tail); + service->setEligibility([](const Packet *) { return true; }, [](Packet *) { return false; }); + auto ordinary = service->extractFramesToTransmit(legacy); + ASSERT(ordinary != nullptr && ordinary->size() == 1 && ordinary->front() == head); + ASSERT(legacy->getNumPackets() == 1 && legacy->getPacket(0) == tail); + take(ordinary->front()); + delete ordinary->front(); + delete ordinary; + std::cout << "Block Ack DATA selection assertions passed.\n"; + endSimulation(); + } + public: + DataSelectionTest() : cSimpleModule(65536) {} +}; +Define_Module(DataSelectionTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.queueing.base.PacketQueueBase; +import inet.queueing.queue.CompoundPacketQueueBase; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +simple LegacySelectionQueue extends PacketQueueBase { parameters: @class(::LegacySelectionQueue); } +module CompatibilityCompoundQueue extends CompoundPacketQueueBase { + parameters: + @class(::CompatibilityCompoundQueue); + submodules: + provider: LegacySelectionQueue; + connections: + in --> provider.in; + provider.out --> out; +} +module DataSelectionHcf extends Hcf { parameters: @class(::DataSelectionHcf); } +simple DataSelectionTest { parameters: int scenario; @class(::DataSelectionTest); } +network DataSelectionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + legacy: LegacySelectionQueue; + test: DataSelectionTest; +} + +%inifile: omnetpp.ini +[General] +network = DataSelectionNetwork +sim-time-limit = 1s +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0..3} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.isBlockAckSupported = false +**.wlan[*].mac.hcf.typename = "DataSelectionHcf" +**.wlan[*].mac.hcf.edca.edcaf[*].pendingQueue.typename = ${queueType="CompoundPendingQueue","CompatibilityCompoundQueue","CompatibilityCompoundQueue","CompatibilityCompoundQueue" ! scenario} +**.pendingQueue.classifier.classifierClass = "inet::ieee80211::MgmtOverMulticastOverUnicastClassifier" +**.pendingQueue.prioritizer.reverseOrder = true +**.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.originatorMacDataService.msduAggregationPolicy.qOsCheck = true +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 10000B +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack DATA selection assertions passed. + +%contains: stdout +Compound compatibility scenario 3 passed. diff --git a/tests/module/Ieee80211BlockAckDeferral_1.test b/tests/module/Ieee80211BlockAckDeferral_1.test new file mode 100644 index 00000000000..89da8e14084 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDeferral_1.test @@ -0,0 +1,393 @@ +%description: +Check Basic setup deferral through wireless DATA, ADDBA, ACK, and DELBA exchanges. +The receiver holds its actual response queue until 150 ms after request ACK. +The 100 ms default must expire; a 250 ms timeout must accept the delayed response. +Other runs check first-DATA numbering, deferred voice versus eligible video, and genuine collision. +Request delays of 100 ms and 450 ms reverse the response deadline and DATA lifetime order. +Newer DATA must preserve the response deadline after the original DATA expires. +RTS failure and lifetime expiry must retire an agreement before its first DATA transmission. +Spaced DATA must start setup before later extraction after an earlier eligible DATA completes. +The same rule applies when greedy A-MSDU aggregation empties the earlier queue. +All runs use seed 0 and zero contention windows. + +%extraargs: -r 0..11 + +%file: Deferral.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" + +using namespace inet; +using namespace inet::ieee80211; + +class DeferralHcf : public Hcf { + public: + int requests = 0; + int delbas = 0; + int dataVoice = 0; + int dataVideo = 0; + int dataAfterRequest = 0; + int aggregatesAccepted = 0; + int requestStart = -1; + int firstData = -1; + simtime_t requestAccepted = SIMTIME_MAX; + simtime_t requestAck = SIMTIME_MAX; + simtime_t observedDeadline = SIMTIME_MAX; + simtime_t responseQueued = SIMTIME_MAX; + simtime_t responseAccepted = SIMTIME_MAX; + simtime_t responseReceived = SIMTIME_MAX; + bool holdResponse = false; + bool holdRequest = false; + bool dropProtectedCts = false; + int droppedCts = 0; + int firstRtsSequence = -1; + int rtsFailures = 0; + bool dataEligible(Packet *frame) const { return isFrameEligible(frame, false); } + bool hasAgreement(Tid tid) const { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(MacAddress("02:00:00:00:00:02"), tid); + return agreement && agreement->getIsAddbaResponseReceived(); + } + bool deferred(Tid tid) const { + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + return procedures.count({MacAddress("02:00:00:00:00:02"), tid}) != 0; + } + bool contending(AccessCategory ac) const { + return check_and_cast(edca->getEdcaf(ac)->getSubmodule("contention"))->isContentionInProgress(); + } + void processMgmtFrame(Packet *frame, const Ptr& header) override { + if (dynamicPtrCast(header) && responseQueued == SIMTIME_MAX) { + responseQueued = simTime(); + if (holdResponse) + rx->setContentionBlocked(true); + } + Hcf::processMgmtFrame(frame, header); + } + protected: + void transmitControlResponseFrame(Packet *response, const Ptr& responseHeader, + Packet *received, const Ptr& receivedHeader) override { + if (dropProtectedCts && droppedCts == 0 && responseAccepted != SIMTIME_MAX && dynamicPtrCast(receivedHeader)) { + droppedCts++; + delete response; + return; + } + Hcf::transmitControlResponseFrame(response, responseHeader, received, receivedHeader); + } + void originatorProcessRtsProtectionFailed(Packet *frame) override { + if (requestStart >= 0) { + auto data = dynamicPtrCast(frame->peekAtFront()); + if (data && data->getTid() == 6) { + rtsFailures++; + if (firstRtsSequence < 0) + firstRtsSequence = data->getSequenceNumber().get(); + } + } + Hcf::originatorProcessRtsProtectionFailed(frame); + } + void frameSequenceFinished() override { + Hcf::frameSequenceFinished(); + if (holdRequest && requestAccepted == SIMTIME_MAX && deferred(6)) + rx->setContentionBlocked(true); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + take(frame); + delete frame; + } + } + bool transmitFrame(Packet *frame, simtime_t ifs) override { + auto header = frame->peekAtFront(); + bool accepted = Hcf::transmitFrame(frame, ifs); + if (!accepted) + return false; + if (auto request = dynamicPtrCast(header)) { + requests++; + if (requestStart < 0) { + requestStart = request->getStartingSequenceNumber().get(); + requestAccepted = simTime(); + ASSERT(observedDeadline == SIMTIME_MAX); + } + } + if (dynamicPtrCast(header)) + delbas++; + if (dynamicPtrCast(header) && responseAccepted == SIMTIME_MAX) + responseAccepted = simTime(); + if (auto data = dynamicPtrCast(header)) { + if (data->getAMsduPresent()) aggregatesAccepted++; + if (data->getTid() == 6) dataVoice++; + if (data->getTid() == 5) dataVideo++; + if (requestStart >= 0 && data->getTid() == 6) { + if (firstData < 0) firstData = data->getSequenceNumber().get(); + dataAfterRequest++; + } + } + return true; + } + void originatorProcessReceivedControlFrame(Packet *frame, const Ptr& header, + Packet *last, const Ptr& lastHeader, AccessCategory ac) override { + bool request = dynamicPtrCast(lastHeader) != nullptr; + Hcf::originatorProcessReceivedControlFrame(frame, header, last, lastHeader, ac); + if (request && requestAck == SIMTIME_MAX) { + requestAck = simTime(); + auto it = originatorBlockAckAgreementHandler->getProcedures().find({MacAddress("02:00:00:00:00:02"), 6}); + if (it != originatorBlockAckAgreementHandler->getProcedures().end()) + observedDeadline = it->second.responseDeadline; + } + } + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + if (dynamicPtrCast(header) && responseReceived == SIMTIME_MAX) + responseReceived = simTime(); + Hcf::recipientProcessReceivedManagementFrame(header); + } +}; +Define_Module(DeferralHcf); + +class DeferralTest : public cSimpleModule, public cListener { + int collisions = 0; + int videoRetryDrops = 0; + int lifetimeDrops = 0; + protected: + void receiveSignal(cComponent *, simsignal_t, unsigned long value, cObject *) override { collisions += value; } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + auto drop = check_and_cast(details); + auto header = dynamicPtrCast(check_and_cast(object)->peekAtFront()); + if (drop->getReason() == RETRY_LIMIT_REACHED && header && header->getTid() == 5) + videoRetryDrops++; + if (drop->getReason() == LIFETIME_EXPIRED) + lifetimeDrops++; + } + Packet *accept(DeferralHcf *hcf, Tid tid) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(MacAddress("02:00:00:00:00:02")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(header->getReceiverAddress()); + auto frame = new Packet(tid == 6 ? "voice-data" : "video-data", header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + hcf->processUpperFrame(frame, header); + return frame; + } + void activity() override { + auto sender = check_and_cast(getModuleByPath("^.sender.wlan[0].mac.hcf")); + auto receiver = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); + auto senderRx = check_and_cast(getModuleByPath("^.sender.wlan[0].mac.rx")); + auto receiverRx = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.rx")); + int scenario = par("scenario"); + sender->subscribe(Hcf::edcaCollisionDetectedSignal, this); + sender->subscribe(packetDroppedSignal, this); + senderRx->setContentionBlocked(true); + if (scenario >= 10) { + auto edca = check_and_cast(sender->getSubmodule("edca")); + accept(sender, 6); + if (scenario == 11) + accept(sender, 6); + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + ASSERT(sender->dataVoice == 1); + ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 1 : 0)); + ASSERT(sender->requests == 0); + ASSERT(edca->getEdcaf(AC_VO)->getPendingQueue()->isEmpty()); + ASSERT(edca->getEdcaf(AC_VO)->getInProgressFrames()->getLength() == 0); + auto later = accept(sender, 6); + bool deferredBeforeExtraction = !sender->dataEligible(later); + if (scenario == 11) + accept(sender, 6); + std::cout << "Later DATA admission at " << simTime() << ": deferred=" << deferredBeforeExtraction << ".\n"; + wait(SimTime(5, SIMTIME_MS)); + std::cout << "Spaced DATA: requests=" << sender->requests << ", DATA accepted=" << sender->dataVoice + << ", aggregates=" << sender->aggregatesAccepted << ", request start=" << sender->requestStart + << ", first DATA=" << sender->firstData << ".\n"; + ASSERT(deferredBeforeExtraction); + ASSERT(sender->requests == 1 && sender->hasAgreement(6)); + ASSERT(sender->requestStart == 1 && sender->firstData == sender->requestStart); + ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 2 : 0)); + } + else if (scenario == 4) { + accept(sender, 6); + accept(sender, 5); + ASSERT(sender->contending(AC_VO) && sender->contending(AC_VI)); + senderRx->setContentionBlocked(false); + wait(SimTime(50, SIMTIME_US)); + ASSERT(sender->dataVoice == 1 && sender->dataVideo == 0); + ASSERT(collisions == 1 && videoRetryDrops == 1); + } + else { + receiver->holdResponse = scenario != 2 && scenario < 8; + receiver->dropProtectedCts = scenario >= 8; + sender->holdRequest = scenario >= 5 && scenario != 8; + accept(sender, 6); + accept(sender, 6); + auto deferredFrame = accept(sender, 6); + auto deferredId = deferredFrame->getId(); + ASSERT(sender->contending(AC_VO)); + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + if (scenario >= 5 && scenario != 8) { + ASSERT(sender->requests == 0 && sender->requestAck == SIMTIME_MAX); + ASSERT(sender->deferred(6)); + auto release = SimTime(scenario == 9 ? 500 : scenario == 5 ? 100 : 450, SIMTIME_MS); + wait(release - simTime()); + sender->holdRequest = false; + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + } + std::cout << "Requests=" << sender->requests << ", start=" << sender->requestStart << ", voice DATA=" << sender->dataVoice << ".\n"; + ASSERT(sender->requests == 1); + ASSERT(sender->requestStart == sender->dataVoice - sender->dataAfterRequest); + ASSERT(sender->requestAck != SIMTIME_MAX); + ASSERT(sender->requestAck > sender->requestAccepted); + auto timeout = SimTime(scenario == 1 ? 250 : 100, SIMTIME_MS); + ASSERT(sender->observedDeadline == sender->requestAck + timeout); + if (scenario >= 8) { + if (scenario == 9) + wait(SimTime(560, SIMTIME_MS) - simTime()); + ASSERT(sender->rtsFailures >= 1); + ASSERT(sender->firstRtsSequence == sender->requestStart); + ASSERT(sender->dataAfterRequest == 0 && sender->firstData == -1); + ASSERT(!sender->hasAgreement(6)); + ASSERT(sender->delbas == 1); + ASSERT(lifetimeDrops == (scenario == 9 ? 1 : 0)); + } + else if (scenario >= 5) { + auto edca = check_and_cast(sender->getSubmodule("edca")); + auto lifetime = edca->getLifetimeHandler(); + ASSERT(lifetime->getDeadline(deferredFrame) == SimTime(512, SIMTIME_MS)); + if (scenario == 5) { + ASSERT(sender->observedDeadline < SimTime(512, SIMTIME_MS)); + wait(sender->observedDeadline + SimTime(5, SIMTIME_MS) - simTime()); + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 1); + ASSERT(lifetimeDrops == 0); + } + else { + ASSERT(sender->observedDeadline > SimTime(512, SIMTIME_MS)); + if (scenario == 7) { + wait(SimTime(480, SIMTIME_MS) - simTime()); + accept(sender, 6); + } + wait(SimTime(513, SIMTIME_MS) - simTime()); + ASSERT(lifetimeDrops == 1 && lifetime->findFrame(deferredId) == nullptr); + if (scenario == 6) + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 0); + else { + ASSERT(sender->delbas == 0 && sender->deferred(6)); + wait(sender->observedDeadline + SimTime(5, SIMTIME_MS) - simTime()); + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 1); + } + } + std::cout << "Request release=" << SimTime(scenario == 5 ? 100 : 450, SIMTIME_MS) << ", request ACK=" << sender->requestAck + << ", response deadline=" << sender->observedDeadline << ", original DATA deadline=0.512.\n"; + } + else if (scenario == 2) { + ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->firstData == sender->requestStart); + } + else { + ASSERT(sender->deferred(6)); + ASSERT(sender->dataAfterRequest == 0); + ASSERT(deferredFrame->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(!sender->contending(AC_VO)); + std::cout << "The pre-setup VO request resolved. Deferred VO has no contention request at " << simTime() << ".\n"; + if (scenario == 3) { + accept(sender, 5); + wait(SimTime(5, SIMTIME_MS)); + ASSERT(sender->dataVideo == 1); + ASSERT(sender->dataAfterRequest == 0); + ASSERT(collisions == 0 && videoRetryDrops == 0); + } + else { + wait(sender->requestAck + SimTime(150, SIMTIME_MS) - simTime()); + receiverRx->setContentionBlocked(false); + wait(SimTime(10, SIMTIME_MS)); + if (scenario == 1) { + ASSERT(receiver->responseAccepted != SIMTIME_MAX); + ASSERT(receiver->responseAccepted >= sender->requestAck + SimTime(150, SIMTIME_MS)); + } + std::cout << "Request ACK=" << sender->requestAck << ", response deadline=" << sender->observedDeadline + << ", response queued=" << receiver->responseQueued << ", response Tx accepted=" << receiver->responseAccepted << ".\n"; + if (scenario == 0) { + ASSERT(sender->delbas >= 1); + ASSERT(!sender->hasAgreement(6)); + } + else { + ASSERT(sender->delbas == 0); + ASSERT(sender->hasAgreement(6)); + ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->firstData == sender->requestStart); + } + } + } + } + sender->unsubscribe(Hcf::edcaCollisionDetectedSignal, this); + sender->unsubscribe(packetDroppedSignal, this); + std::cout << "Block Ack deferral scenario " << scenario << " passed.\n"; + endSimulation(); + } + public: + DeferralTest() : cSimpleModule(65536) {} +}; +Define_Module(DeferralTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.blockack.OriginatorBlockAckAgreementPolicy; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module DeferralHcf extends Hcf { parameters: @class(::DeferralHcf); } +simple LongTimeoutPolicy extends OriginatorBlockAckAgreementPolicy { parameters: addbaFailureTimeout = 250ms; } +simple DeferralTest { parameters: int scenario; @class(::DeferralTest); } +network DeferralNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sender: WirelessHost; + receiver: WirelessHost; + test: DeferralTest; +} + +%inifile: omnetpp.ini +[General] +network = DeferralNetwork +sim-time-limit = 1s +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0..11} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "DeferralHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} != 4 +**.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.typename = ${policy="OriginatorBlockAckAgreementPolicy","LongTimeoutPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy" ! scenario} +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = ${aggregation="","","","","","","","","","","","BasicMsduAggregationPolicy" ! scenario} +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = ${scenario} >= 8 && ${scenario} <= 9 ? 1B : 2346B +**.wlan[*].mac.hcf.rtsPolicy.ctsTimeout = ${scenario} == 9 ? 50ms : -1s +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = ${scenario} == 9 ? 10 : 1 +*.sender.wlan[0].address = "02:00:00:00:00:01" +*.receiver.wlan[0].address = "02:00:00:00:00:02" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sender.mobility.initialX = 0m +*.sender.mobility.initialY = 0m +*.receiver.mobility.initialX = 1m +*.receiver.mobility.initialY = 0m + +%contains: stdout +Block Ack deferral scenario 11 passed. diff --git a/tests/module/Ieee80211BlockAckDeferredCollision_1.test b/tests/module/Ieee80211BlockAckDeferredCollision_1.test new file mode 100644 index 00000000000..8bc3eef8be4 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDeferredCollision_1.test @@ -0,0 +1,203 @@ +%description: +Check a pending EDCA request after production inactivity teardown defers its DATA. +The fixture supplies an accepted Basic agreement; it does not test negotiation. +VI and BE request the same grant time, at 34 microseconds. +The VI agreement expires at 33 microseconds and queues DELBA in VO. +VO uses six AIFS slots and grants later, at 70 microseconds. +Deferred VI must not cause collision feedback or disposal of eligible BE DATA. +The control retains eligible VI and must preserve the genuine collision. +The third run gives BE an accepted agreement with SSN zero and two DATA frames. +An internal collision discards sequence zero before transmission. +HCF must retire that agreement and queue DELBA before a successor can transmit. + +%extraargs: -r 0..2 + +%file: DeferredCollision.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" + +using namespace inet; +using namespace inet::ieee80211; + +class DeferredCollisionOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(simtime_t timeout, Tid tid) { + MacAddress peer("02:00:00:00:00:02"); + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(0), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; + +class DeferredCollisionHcf : public Hcf { + public: + int videoAccepted = 0; + int bestEffortAccepted = 0; + int delbas = 0; + void prepare(bool expire, Tid tid) { + Enter_Method("prepare"); + delete originatorBlockAckAgreementHandler; + auto handler = new DeferredCollisionOriginator(); + originatorBlockAckAgreementHandler = handler; + handler->accept(expire ? SimTime(33, SIMTIME_US) : SIMTIME_ZERO, tid); + if (expire) + scheduleInactivityTimer(); + } + bool contending(AccessCategory ac) const { + return check_and_cast(edca->getEdcaf(ac)->getSubmodule("contention"))->isContentionInProgress(); + } + bool eligible(AccessCategory ac) { return hasFrameToTransmit(ac); } + int queued(AccessCategory ac) const { return edca->getEdcaf(ac)->getPendingQueue()->getNumPackets(); } + bool hasAgreement(Tid tid) const { + return originatorBlockAckAgreementHandler->getAgreement(MacAddress("02:00:00:00:00:02"), tid) != nullptr; + } + protected: + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbas++; + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *frame, simtime_t ifs) override { + auto header = dynamicPtrCast(frame->peekAtFront()); + bool accepted = Hcf::transmitFrame(frame, ifs); + if (accepted && header) { + if (header->getTid() == 5) videoAccepted++; + if (header->getTid() == 0) bestEffortAccepted++; + } + return accepted; + } +}; +Define_Module(DeferredCollisionHcf); + +class DeferredCollisionTest : public cSimpleModule, public cListener { + int collisions = 0; + int bestEffortRetryDrops = 0; + protected: + void receiveSignal(cComponent *, simsignal_t, unsigned long count, cObject *) override { collisions += count; } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + auto drop = check_and_cast(details); + auto header = dynamicPtrCast(check_and_cast(object)->peekAtFront()); + if (drop->getReason() == RETRY_LIMIT_REACHED && header && header->getTid() == 0) + bestEffortRetryDrops++; + } + void accept(DeferredCollisionHcf *hcf, Tid tid) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setReceiverAddress(MacAddress("02:00:00:00:00:02")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(header->getReceiverAddress()); + auto frame = new Packet(tid == 5 ? "video-data" : "best-effort-data", header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + hcf->processUpperFrame(frame, header); + } + void activity() override { + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + int scenario = par("scenario"); + bool expire = scenario == 0; + Tid agreementTid = scenario == 2 ? 0 : 5; + hcf->prepare(expire, agreementTid); + hcf->subscribe(Hcf::edcaCollisionDetectedSignal, this); + hcf->subscribe(packetDroppedSignal, this); + accept(hcf, 5); + accept(hcf, 0); + if (scenario == 2) + accept(hcf, 0); + ASSERT(hcf->contending(AC_VI) && hcf->contending(AC_BE)); + wait(SimTime(33500, SIMTIME_NS)); + ASSERT(hcf->hasAgreement(agreementTid) == !expire); + ASSERT(hcf->eligible(AC_VI) == !expire); + ASSERT(hcf->eligible(AC_BE)); + std::cout << "At " << simTime() << ": VI eligible=" << hcf->eligible(AC_VI) + << ", VI pending request=" << hcf->contending(AC_VI) + << ", BE pending request=" << hcf->contending(AC_BE) + << ", VO pending request=" << hcf->contending(AC_VO) << ".\n"; + wait(SimTime(16500, SIMTIME_NS)); + std::cout << "At " << simTime() << ": VI DATA accepted=" << hcf->videoAccepted + << ", BE DATA accepted=" << hcf->bestEffortAccepted + << ", collisions=" << collisions << ", BE retry drops=" << bestEffortRetryDrops << ".\n"; + ASSERT(hcf->videoAccepted == (expire ? 0 : 1)); + ASSERT(hcf->bestEffortAccepted == (expire ? 1 : 0)); + ASSERT(collisions == (expire ? 0 : 1)); + ASSERT(bestEffortRetryDrops == (expire ? 0 : 1)); + if (scenario == 2) { + ASSERT(!hcf->hasAgreement(0)); + ASSERT(hcf->delbas == 1); + ASSERT(!hcf->eligible(AC_BE)); + ASSERT(hcf->queued(AC_BE) == 1); + ASSERT(hcf->bestEffortAccepted == 0); + std::cout << "First-DATA collision retires the agreement before successor transmission.\n"; + } + hcf->unsubscribe(Hcf::edcaCollisionDetectedSignal, this); + hcf->unsubscribe(packetDroppedSignal, this); + std::cout << "Deferred collision scenario " << par("scenario").intValue() << " passed.\n"; + endSimulation(); + } + public: + DeferredCollisionTest() : cSimpleModule(65536) {} +}; +Define_Module(DeferredCollisionTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module DeferredCollisionHcf extends Hcf { parameters: @class(::DeferredCollisionHcf); } +simple DeferredCollisionTest { parameters: int scenario; @class(::DeferredCollisionTest); } +network DeferredCollisionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: DeferredCollisionTest; +} + +%inifile: omnetpp.ini +[General] +network = DeferredCollisionNetwork +sim-time-limit = 1ms +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0,1,2} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "DeferredCollisionHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +*.host.wlan[0].mac.hcf.edca.edcaf[3].aifsn = 6 +**.wlan[*].mac.hcf.edca.edcaf[*].aifsn = 2 +**.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = 1 +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Deferred collision scenario 1 passed. + +%contains: stdout +First-DATA collision retires the agreement before successor transmission. diff --git a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test new file mode 100644 index 00000000000..33429c70e6a --- /dev/null +++ b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test @@ -0,0 +1,297 @@ +%description: +Check the sequence boundary after ordinary originator teardown of fragmented DATA. +IEEE Std 802.11-2024, 11.5.3.4 requires the next MPDU to use a later sequence +when the originator discards the last transmitted MPDU. +The fixture supplies accepted Basic agreements and injects immediate ACKs. +Production fragmentation, inactivity expiry, retirement, and transmission remain active. +Runs cover sequence 0 and wraparound at 4095, with expiry during transmission +or after the first fragment receives its Normal Ack. +Later intact DATA and an unrelated peer must retain their transmissions. +Additional runs transmit an earlier DATA unit before a gap in sequence allocation. +The fixture extracts and disposes of the skipped DATA through the production allocator. +Distances 2047, 2048, 2049, and 4096 cover the half-range boundary and a full wrap. + +%extraargs: -r 0..19 + +%file: FragmentRetirement.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class FragmentRetirementOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, int start, simtime_t timeout) { + auto agreement = new OriginatorBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; + +class FragmentRetirementHcf : public Hcf { + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress other = MacAddress("02:00:00:00:00:03"); + int start = 0; + int distance = 0; + bool acknowledgedFirst = false; + bool firstCompleted = false; + bool firstAcknowledged = false; + bool retired = false; + bool successorSent = false; + bool otherSent = false; + int delbas = 0; + + void skipData(int count) { + auto queue = edca->getEdcaf(AC_VI)->getPendingQueue(); + for (int i = 0; i < count; i++) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(5); + auto packet = new Packet("sequence-setup", header); + packet->insertAtBack(makeShared()); + queue->enqueuePacket(packet); + auto frames = originatorDataService->extractFramesToTransmit(queue); + for (auto frame : *frames) { + take(frame); + delete frame; + } + delete frames; + } + } + + void startRetirement() { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 5); + agreement->setStartingSequenceNumber(SequenceNumberCyclic(start)); + agreement->setBlockAckTimeoutValue(SimTime(1024, SIMTIME_US)); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(); + scheduleAfter(SimTime(acknowledgedFirst ? 650 : 974, SIMTIME_US), new cMessage("queue-data")); + } + + void queueData(const char *name, MacAddress address, int length) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(5); + header->setReceiverAddress(address); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(address); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(length))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + + public: + void begin() { + Enter_Method("begin"); + start = par("startSequence"); + distance = par("sequenceDistance"); + acknowledgedFirst = par("acknowledgedFirst"); + delete originatorBlockAckAgreementHandler; + auto handler = new FragmentRetirementOriginator(); + originatorBlockAckAgreementHandler = handler; + handler->accept(peer, start, SIMTIME_ZERO); + handler->accept(other, 0, SIMTIME_ZERO); + // Advance the production allocator without a radio exchange for each earlier sequence. + skipData(start); + subscribe(blockAckAgreementDeletedSignal, this); + if (distance == 0) + startRetirement(); + else { + // Force Normal Ack for the earlier DATA, independent of the fragment ACK policy. + handler->getAgreement(peer, 5)->setStartingSequenceNumber(SequenceNumberCyclic(start) + 1); + queueData("earlier-data", peer, 20); + } + scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verify")); + } + + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal != blockAckAgreementDeletedSignal) { + Hcf::receiveSignal(source, signal, object, details); + return; + } + ASSERT(!retired); + retired = true; + ASSERT(firstCompleted == acknowledgedFirst); + ASSERT(firstAcknowledged == acknowledgedFirst); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 5) == nullptr); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) != nullptr); + auto frames = edca->getEdcaf(AC_VI)->getInProgressFrames(); + for (int i = 0; i < frames->getLength(); i++) { + auto data = dynamicPtrCast(frames->getFrames(i)->peekAtFront()); + ASSERT(!data || data->getReceiverAddress() != peer || data->getSequenceNumber() != SequenceNumberCyclic(start)); + } + } + + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto data = dynamicPtrCast(packet->peekAtFront()); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && data) { + if (packet->isName("earlier-data")) { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start)); + ASSERT(data->getAckPolicy() == NORMAL_ACK); + return accepted; + } + if (data->getReceiverAddress() == peer) { + if (!retired) { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start)); + ASSERT(data->getFragmentNumber() == 0); + } + else { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start) + 1); + ASSERT(data->getFragmentNumber() == 0 && !data->getMoreFragments()); + successorSent = true; + } + } + else { + ASSERT(data->getReceiverAddress() == other); + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(0)); + otherSent = true; + } + } + return accepted; + } + + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto data = dynamicPtrCast(header); + bool earlier = packet->isName("earlier-data"); + bool first = !earlier && data && data->getReceiverAddress() == peer && data->getSequenceNumber() == SequenceNumberCyclic(start); + bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); + if (first) + firstCompleted = true; + if (dynamicPtrCast(header)) + delbas++; + Hcf::transmissionComplete(packet, header); + if (ack) + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage(earlier ? "ack-earlier" : first ? "ack-first" : "ack")); + } + + void handleMessage(cMessage *message) override { + if (message->isName("queue-data")) { + delete message; + queueData("fragmented-data", peer, 400); + queueData("intact-successor", peer, 20); + queueData("other-peer", other, 20); + } + else if (message->isName("ack") || message->isName("ack-first") || message->isName("ack-earlier")) { + bool first = message->isName("ack-first"); + bool earlier = message->isName("ack-earlier"); + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + if (first) + firstAcknowledged = true; + if (earlier) { + skipData(distance - 1); + start = (start + distance) % 4096; + startRetirement(); + } + } + else if (message->isName("verify")) { + delete message; + ASSERT(retired && firstCompleted && successorSent && otherSent); + ASSERT(delbas == 1); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) != nullptr); + unsubscribe(blockAckAgreementDeletedSignal, this); + std::cout << "Fragment retirement passed: start=" << start << ", acknowledgedFirst=" << acknowledgedFirst + << ", distance=" << distance << std::endl; + endSimulation(); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(FragmentRetirementHcf); + +class FragmentRetirementDriver : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(FragmentRetirementDriver); +} + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module FragmentRetirementHcf extends Hcf { + parameters: + int startSequence; + int sequenceDistance; + bool acknowledgedFirst; + @class(::inet::ieee80211::FragmentRetirementHcf); +} +simple FragmentRetirementDriver { parameters: @class(::inet::ieee80211::FragmentRetirementDriver); } +network FragmentRetirementNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + driver: FragmentRetirementDriver; +} + +%inifile: omnetpp.ini +[General] +network = FragmentRetirementNetwork +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "FragmentRetirementHcf" +**.wlan[*].mac.hcf.startSequence = ${start=0,4095} +**.wlan[*].mac.hcf.sequenceDistance = ${distance=0,2047,2048,2049,4096} +**.wlan[*].mac.hcf.acknowledgedFirst = ${acked=false,true} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 200B +**.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = ${acked} ? 0B : 1000B +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].aifsn = 20 +**.wlan[*].mac.hcf.edca.edcaf[2].txopProcedure.txopLimit = 0s +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Fragment retirement passed: start=0, acknowledgedFirst=0 +%contains: stdout +Fragment retirement passed: start=0, acknowledgedFirst=1 +%contains: stdout +Fragment retirement passed: start=4095, acknowledgedFirst=0 +%contains: stdout +Fragment retirement passed: start=4095, acknowledgedFirst=1 diff --git a/tests/module/Ieee80211BlockAckLifecycle_1.test b/tests/module/Ieee80211BlockAckLifecycle_1.test new file mode 100644 index 00000000000..eb6d073cafd --- /dev/null +++ b/tests/module/Ieee80211BlockAckLifecycle_1.test @@ -0,0 +1,358 @@ +%description: +Check Basic teardown through initialized HCF and production receive paths. Each role has agreements for two peers and two TIDs. Deletion callbacks inspect the selected agreement after its removal. Recipient teardown releases complete units behind a gap before the callback. Absent-agreement DATA and BAR cause recipient-role DELBA without agreement allocation. The fixture supplies accepted agreements; it does not test negotiation or wireless transmission. + +The inactivity timer uses the earliest absolute deadline from both agreement roles. The fixture checks that derived deadline and the later expiration of the opposite role. + +%extraargs: -r 0..9 + +%file: Lifecycle.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class LifecycleOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid, int start = 100, simtime_t timeout = SIMTIME_ZERO) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; +class LifecycleRecipient : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid, int start = 100, simtime_t timeout = SIMTIME_ZERO) { + auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), 64, timeout); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; +class LifecycleHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress other = MacAddress("02:00:00:00:00:03"); + std::vector delivered; + int deletions = 0; + int delbas = 0; + int controlResponses = 0; + bool selectedRecipient = false; + bool requireRelease = false; + bool strictOpposite = true; + bool dynamicRole = false; + void prepare(int start = 100, simtime_t originatorTimeout = SIMTIME_ZERO, simtime_t recipientTimeout = SIMTIME_ZERO) { + Enter_Method("prepare"); + delete originatorBlockAckAgreementHandler; + delete recipientBlockAckAgreementHandler; + auto originator = new LifecycleOriginator(); + auto recipient = new LifecycleRecipient(); + originatorBlockAckAgreementHandler = originator; + recipientBlockAckAgreementHandler = recipient; + for (auto address : {peer, other}) + for (Tid tid : {5, 6}) { + bool selected = address == peer && tid == 5; + originator->accept(address, tid, start, selected ? originatorTimeout : SIMTIME_ZERO); + recipient->accept(address, tid, start, selected ? recipientTimeout : SIMTIME_ZERO); + } + subscribe(blockAckAgreementDeletedSignal, this); + } + void checkInactivityDeadline(simtime_t expectedDeadline) { + Enter_Method("checkInactivityDeadline"); + scheduleInactivityTimer(); + ASSERT(inactivityTimer->isScheduled()); + ASSERT(inactivityTimer->getArrivalTime() == expectedDeadline); + } + bool hasInactivityTimer() const { return inactivityTimer->isScheduled(); } + void retireRecipientLocally() { + Enter_Method("retireRecipientLocally"); + selectedRecipient = true; + beginTeardown(peer, 5, false, 37); + } + bool hasOriginator(MacAddress address, Tid tid) { + return originatorBlockAckAgreementHandler->getAgreement(address, tid) != nullptr; + } + bool hasRecipient(MacAddress address, Tid tid) { + return recipientBlockAckAgreementHandler->getAgreement(tid, address) != nullptr; + } + void receiveDelba(bool initiator, int sequence) { + Enter_Method("receiveDelba"); + selectedRecipient = initiator; + auto header = makeShared(); + header->setTransmitterAddress(peer); + header->setReceiverAddress(mac->getAddress()); + header->setTid(5); + header->setInitiator(initiator); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + auto packet = new Packet("delba", header); + packet->addTag()->setMode( + physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + void receiveData(int sequence, const char *name, MacAddress address, bool incomplete = false) { + Enter_Method("receiveData"); + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(address); + header->setReceiverAddress(mac->getAddress()); + header->setAddress3(address); + header->setTid(5); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setMoreFragments(incomplete); + auto packet = new Packet(name, header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + void receiveBar(MacAddress address, int fragment = 0) { + Enter_Method("receiveBar"); + auto header = makeShared(); + header->setTransmitterAddress(address); + header->setReceiverAddress(mac->getAddress()); + header->setTidInfo(5); + header->setFragmentNumber(fragment); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("bar", header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *payload, cObject *details) override { + if (signal != blockAckAgreementDeletedSignal) { + Hcf::receiveSignal(source, signal, payload, details); + return; + } + if (requireRelease) + ASSERT((delivered == std::vector{"second", "third"})); + ASSERT(payload != nullptr); + if (dynamicRole) + selectedRecipient = dynamic_cast(payload) != nullptr; + if (selectedRecipient) { + auto agreement = check_and_cast(payload); + ASSERT(agreement->getBlockAckRecord()->getOriginatorAddress() == peer); + ASSERT(agreement->getBlockAckRecord()->getTid() == 5); + ASSERT(!hasRecipient(peer, 5)); + if (strictOpposite) + ASSERT(hasOriginator(peer, 5)); + ASSERT(agreement->getBufferSize() == 64); + } + else { + auto agreement = check_and_cast(payload); + ASSERT(agreement->getReceiverAddr() == peer); + ASSERT(agreement->getTid() == 5); + ASSERT(!hasOriginator(peer, 5)); + if (strictOpposite) + ASSERT(hasRecipient(peer, 5)); + ASSERT(agreement->getBufferSize() == 64); + } + ASSERT(hasOriginator(other, 5)); + ASSERT(hasRecipient(other, 5)); + ASSERT(hasOriginator(peer, 6)); + ASSERT(hasRecipient(peer, 6)); + deletions++; + } + void sendUp(const std::vector& frames) override { + for (auto packet : frames) { + if (dynamicPtrCast(packet->peekAtFront())) + delivered.push_back(packet->getName()); + take(packet); + delete packet; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + controlResponses++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (auto delba = dynamicPtrCast(header)) { + if (!dynamicRole) + ASSERT(!delba->getInitiator()); + delbas++; + } + delete packet; + } +}; +Define_Module(LifecycleHcf); +class LifecycleTest : public cSimpleModule { + protected: + LifecycleHcf *hcf = nullptr; + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + if (message->isName("recipientExpiry")) { + delete message; + ASSERT(hcf->hasOriginator(hcf->peer, 5)); + ASSERT(!hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + ASSERT((hcf->delivered == std::vector{"second", "third"})); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + if (message->isName("firstExpiry")) { + delete message; + ASSERT(!hcf->hasOriginator(hcf->peer, 5)); + ASSERT(hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + scheduleAt(SimTime(5000001, SIMTIME_NS), new cMessage("secondExpiry")); + return; + } + if (message->isName("secondExpiry")) { + delete message; + ASSERT(!hcf->hasOriginator(hcf->peer, 5)); + ASSERT(!hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 2); + ASSERT(hcf->delbas == 2); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + if (message->isName("zeroExpiry")) { + delete message; + ASSERT(hcf->hasOriginator(hcf->peer, 5)); + ASSERT(hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 0); + ASSERT(hcf->delbas == 0); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + delete message; + hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + int scenario = par("scenario"); + hcf->prepare(scenario == 5 ? 4094 : 100, + scenario == 6 ? SimTime(2, SIMTIME_MS) : SIMTIME_ZERO, + scenario == 6 ? SimTime(4, SIMTIME_MS) : scenario == 9 ? SimTime(2, SIMTIME_MS) : SIMTIME_ZERO); + if (scenario >= 8) { + hcf->requireRelease = true; + hcf->selectedRecipient = true; + hcf->receiveData(101, "second", hcf->peer); + hcf->receiveData(102, "third", hcf->peer); + ASSERT(hcf->delivered.empty()); + if (scenario == 8) { + hcf->retireRecipientLocally(); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + hcf->retireRecipientLocally(); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + std::cout << "Block Ack lifecycle assertions passed.\n"; + } + else { + hcf->checkInactivityDeadline(SimTime(3, SIMTIME_MS)); + scheduleAt(SimTime(3000001, SIMTIME_NS), new cMessage("recipientExpiry")); + } + return; + } + if (scenario >= 6) { + if (scenario == 6) { + hcf->strictOpposite = false; + hcf->dynamicRole = true; + hcf->checkInactivityDeadline(SimTime(3, SIMTIME_MS)); + scheduleAt(SimTime(3000001, SIMTIME_NS), new cMessage("firstExpiry")); + } + else + scheduleAt(SimTime(6, SIMTIME_MS), new cMessage("zeroExpiry")); + return; + } + if (scenario <= 2 || scenario == 5) { + if (scenario == 2 || scenario == 5) { + hcf->requireRelease = true; + if (scenario == 5) + hcf->receiveData(4094, "incomplete-frag0", hcf->peer, true); + hcf->receiveData(scenario == 5 ? 4095 : 101, "second", hcf->peer); + hcf->receiveData(scenario == 5 ? 0 : 102, "third", hcf->peer); + ASSERT(hcf->delivered.empty()); + } + hcf->receiveDelba(scenario != 0, 20); + ASSERT(hcf->deletions == 1); + hcf->receiveDelba(scenario != 0, 21); + ASSERT(hcf->deletions == 1); + if (scenario == 2 || scenario == 5) + ASSERT((hcf->delivered == std::vector{"second", "third"})); + } + else { + const MacAddress absent("02:00:00:00:00:04"); + if (scenario == 4) { + hcf->receiveBar(absent, 1); + ASSERT(hcf->delbas == 0); + ASSERT(hcf->controlResponses == 0); + } + if (scenario == 3) + hcf->receiveData(100, "absent", absent); + else + hcf->receiveBar(absent); + ASSERT(hcf->delivered.empty()); + ASSERT(hcf->delbas == 1); + ASSERT(hcf->controlResponses == 0); + ASSERT(!hcf->hasRecipient(absent, 5)); + ASSERT(!hcf->hasOriginator(absent, 5)); + if (scenario == 3) + hcf->receiveData(101, "absent-again", absent); + else + hcf->receiveBar(absent); + ASSERT(hcf->delbas == 1); + ASSERT(hcf->delivered.empty()); + } + std::cout << "Block Ack lifecycle assertions passed.\n"; + } +}; +Define_Module(LifecycleTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module LifecycleHcf extends Hcf { parameters: @class(::inet::ieee80211::LifecycleHcf); } +simple LifecycleTest { parameters: int scenario; @class(::inet::ieee80211::LifecycleTest); } +network LifecycleNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: LifecycleTest; +} + +%inifile: omnetpp.ini +[General] +network = LifecycleNetwork +ned-path = .;../../../../src +sim-time-limit = 7ms +seed-set = 0 +*.test.scenario = ${0..9} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "LifecycleHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack lifecycle assertions passed. diff --git a/tests/module/Ieee80211BlockAckNegotiation_1.test b/tests/module/Ieee80211BlockAckNegotiation_1.test new file mode 100644 index 00000000000..3f8b9c11a56 --- /dev/null +++ b/tests/module/Ieee80211BlockAckNegotiation_1.test @@ -0,0 +1,83 @@ +%description: +Check recipient negotiation through the production agreement handler. +The recipient response determines buffer size and inactivity timeout. +The response copies the request dialog token. + +%extraargs: -r 0..3 + +%file: Negotiation.cc +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" + +namespace inet::ieee80211 { +class NegotiationPolicy : public IRecipientBlockAckAgreementPolicy { + public: + bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override { + return variant == BlockAckVariant::BASIC || (localHt && peerHt); + } + bool isAddbaReqAccepted(const Ptr&) override { return true; } + bool isDelbaAccepted(const Ptr&) override { return true; } + simtime_t getBlockAckTimeoutValue() const override { return SimTime(20, SIMTIME_MS); } + bool aMsduSupported() const override { return false; } + bool delayedBlockAckPolicySupported() const override { return false; } + int getMaximumAllowedBufferSize() const override { return 64; } +}; +class NegotiationTest : public cSimpleModule, public IProcedureCallback { + protected: + RecipientBlockAckAgreementHandler handler; + NegotiationPolicy policy; + Ptr response; + void initialize() override { scheduleAt(SIMTIME_ZERO, new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + const MacAddress peer("02:00:00:00:00:02"); + auto request = makeShared(); + request->setTransmitterAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(par("requestSize")); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + request->setBlockAckTimeoutValue(SimTime(40, SIMTIME_MS)); + handler.processReceivedAddbaRequest(request, &policy, this); + ASSERT(response != nullptr); + ASSERT(response->getDialogToken() == 37); + ASSERT(response->getBufferSize() == 64); + ASSERT(response->getBlockAckTimeoutValue() == SimTime(20, SIMTIME_MS)); + auto agreement = handler.getAgreement(5, peer); + ASSERT(agreement != nullptr); + ASSERT(agreement->getBufferSize() == response->getBufferSize()); + ASSERT(agreement->getBlockAckTimeoutValue() == response->getBlockAckTimeoutValue()); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(100)); + std::cout << "Block Ack negotiation assertions passed.\n"; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + response = dynamicPtrCast(header); + delete packet; + } + void transmitControlResponseFrame(Packet *, const Ptr&, + Packet *, const Ptr&) override { throw cRuntimeError("Unexpected control response"); } +}; +Define_Module(NegotiationTest); +} + +%file: test.ned +simple NegotiationTest { + parameters: + int requestSize; + @class(::inet::ieee80211::NegotiationTest); +} +network NegotiationNetwork { submodules: test: NegotiationTest; } + +%inifile: omnetpp.ini +[General] +network = NegotiationNetwork +ned-path = .;../../../../src +sim-time-limit = 1ms +seed-set = 0 +*.test.requestSize = ${0,8,64,128} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Block Ack negotiation assertions passed. diff --git a/tests/module/Ieee80211BlockAckResponseWait_1.test b/tests/module/Ieee80211BlockAckResponseWait_1.test new file mode 100644 index 00000000000..9fc88c204d4 --- /dev/null +++ b/tests/module/Ieee80211BlockAckResponseWait_1.test @@ -0,0 +1,242 @@ +%description: +Check Action reception during the final ADDBA Request ACK wait through initialized Basic HCF. The request uses production contention, transmission, and frame-sequence handling. The fixture supplies the initial request record and injects an incoming Action after transmission. Matching success and rejection must suppress obsolete request failure. Wrong-token and unrelated-Action controls must retain ordinary request failure. The test does not model wireless reception or peer contention. + +%extraargs: -r 0..3 + +%file: ResponseWait.cc +#include +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ResponseWaitOriginator : public OriginatorBlockAckAgreementHandler { + public: + void prepare(const Ptr& request) { createAgreement(request); } +}; +class ResponseWaitHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + int attempts = 0; + int failures = 0; + int acknowledgments = 0; + int delbas = 0; + int linkBreaks = 0; + int requestOutcomes = 0; + bool injected = false; + bool dispatchingAction = false; + void begin() { + Enter_Method("begin"); + subscribe(linkBrokenSignal, this); + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + delete originatorBlockAckAgreementHandler; + auto handler = new ResponseWaitOriginator(); + originatorBlockAckAgreementHandler = handler; + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + handler->prepare(request); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(mac->getAddress()); + data->setReceiverAddress(peer); + data->setAddress3(peer); + data->setTid(5); + auto packet = new Packet("deferred-data", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, data); + Hcf::processMgmtFrame(new Packet("request", request), request); + } + void verify() { + Enter_Method("verify"); + ASSERT(injected); + ASSERT(attempts == 1); + ASSERT(acknowledgments == 1); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 5); + int scenario = par("scenario"); + if (scenario < 2) { + ASSERT(failures == 0); + ASSERT(delbas == 0); + ASSERT(linkBreaks == 0); + ASSERT(requestOutcomes <= 1); + ASSERT(!addbaResponseTimer->isScheduled()); + ASSERT(!startRxTimer->isScheduled()); + if (scenario == 0) { + ASSERT(agreement != nullptr); + ASSERT(agreement->getIsAddbaResponseReceived()); + ASSERT(agreement->getBufferSize() == 32); + } + else + ASSERT(agreement == nullptr || !agreement->getIsAddbaResponseReceived()); + } + else { + ASSERT(failures == 1); + ASSERT(linkBreaks == 1); + ASSERT(requestOutcomes == 1); + ASSERT(agreement == nullptr || !agreement->getIsAddbaResponseReceived()); + } + std::cout << "Block Ack response-wait assertions passed.\n"; + endSimulation(); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == linkBrokenSignal) + linkBreaks++; + else if (signal == Ieee80211Mac::frameTransmissionOutcomeSignal) { + auto packet = check_and_cast(object); + if (dynamicPtrCast(packet->peekAtFront())) + requestOutcomes++; + } + else + Hcf::receiveSignal(source, signal, object, details); + } + void handleMessage(cMessage *message) override { + if (message->isName("injectAction")) { + delete message; + ASSERT(frameSequenceHandler->isSequenceRunning()); + ASSERT(frameSequenceHandler->getContext()->getLastStep()->getType() == IFrameSequenceStep::Type::RECEIVE); + ASSERT(startRxTimer->isScheduled()); + int scenario = par("scenario"); + Ptr header; + if (scenario == 3) { + auto request = makeShared(); + request->setTid(6); + request->setDialogToken(91); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + header = request; + } + else { + auto response = makeShared(); + response->setTid(5); + response->setDialogToken(scenario == 2 ? 38 : 37); + response->setStatusCode(scenario == 1 ? 37 : 0); + response->setBlockAckPolicy(true); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(SIMTIME_ZERO); + header = response; + } + header->setTransmitterAddress(peer); + header->setReceiverAddress(mac->getAddress()); + header->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet("incoming-action", header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + injected = true; + dispatchingAction = true; + processLowerFrame(packet, header); + dispatchingAction = false; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("verify")); + } + else if (message->isName("verify")) { + delete message; + verify(); + } + else + Hcf::handleMessage(message); + } + void originatorProcessTransmittedFrame(Packet *packet) override { + auto request = dynamicPtrCast(packet->peekAtFront()); + Hcf::originatorProcessTransmittedFrame(packet); + if (request) { + attempts++; + scheduleAfter(SimTime(100, SIMTIME_US), new cMessage("injectAction")); + } + } + void originatorProcessFailedFrame(Packet *packet) override { + if (dynamicPtrCast(packet->peekAtFront())) + failures++; + Hcf::originatorProcessFailedFrame(packet); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + if (dynamicPtrCast(packet->peekAtFront())) + ASSERT(!dispatchingAction); + return Hcf::transmitFrame(packet, ifs); + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + acknowledgments++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbas++; + delete packet; + } + void sendUp(const std::vector& frames) override { + for (auto packet : frames) { + take(packet); + delete packet; + } + } +}; +Define_Module(ResponseWaitHcf); +class ResponseWaitTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(ResponseWaitTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ResponseWaitHcf extends Hcf { + parameters: + int scenario; + @class(::inet::ieee80211::ResponseWaitHcf); +} +simple ResponseWaitTest { parameters: @class(::inet::ieee80211::ResponseWaitTest); } +network ResponseWaitNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ResponseWaitTest; +} + +%inifile: omnetpp.ini +[General] +network = ResponseWaitNetwork +ned-path = .;../../../../src +sim-time-limit = 5ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ResponseWaitHcf" +**.wlan[*].mac.hcf.scenario = ${0..3} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.mgmtAndNonQoSRecoveryProcedure.shortRetryLimit = 1 +**.mgmtAndNonQoSRecoveryProcedure.longRetryLimit = 1 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack response-wait assertions passed. diff --git a/tests/module/PacketFilteredPullEffects_1.test b/tests/module/PacketFilteredPullEffects_1.test new file mode 100644 index 00000000000..e51e6c1f850 --- /dev/null +++ b/tests/module/PacketFilteredPullEffects_1.test @@ -0,0 +1,191 @@ +%description: +Check filtered pulls through simple and compound queues. +A filtered pull must preserve ownership, residence metadata, and one pull event per queue layer. +The filter captures a selected packet. Its query must preserve storage and configured scheduler order. + +%extraargs: -r 0..5 + +%file: SelectiveExtraction.cc +#include "inet/common/PacketEventTag.h" +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/queueing/contract/IPacketQueue.h" +#include "inet/queueing/function/PacketFilterFunction.h" +#include "inet/queueing/scheduler/PriorityScheduler.h" + +namespace inet::queueing { +class SelectiveScheduler : public PriorityScheduler { + public: + int getProcessedCount() const { return numProcessedPackets; } +}; +Define_Module(SelectiveScheduler); + +class SelectiveExtractionTest : public cSimpleModule, public cListener { + protected: + IPacketQueue *queue = nullptr; + cModule *queueModule = nullptr; + Packet *first = nullptr; + Packet *second = nullptr; + std::map pulls; + int removals = 0; + + Packet *makePacket(const char *name, bool multicast) { + auto header = makeShared(); + header->setType(ieee80211::ST_DATA_WITH_QOS); + header->addChunkLength(ieee80211::QOSCONTROL_PART_LENGTH); + header->setTid(5); + header->setReceiverAddress(MacAddress(multicast ? "01:00:00:00:00:01" : "02:00:00:00:00:02")); + auto packet = new Packet(name, header); + packet->setArrivalTime(simTime()); + packet->insertAtBack(makeShared(B(20))); + auto timeTag = packet->addRegionTag(); + timeTag->setPacketTotalTimesArraySize(1); + timeTag->setPacketTotalTimes(0, SIMTIME_ZERO); + packet->addRegionTag(); + return packet; + } + + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("enqueue")); + } + + void handleMessage(cMessage *message) override { + bool enqueue = !strcmp(message->getName(), "enqueue"); + delete message; + if (enqueue) { + queueModule = getModuleByPath(par("compound").boolValue() ? "^.compound" : "^.simpleQueue"); + queue = check_and_cast(queueModule); + queueModule->subscribe(packetPulledSignal, this); + queueModule->subscribe(packetRemovedSignal, this); + first = makePacket("first", true); + second = makePacket("second", false); + queue->enqueuePacket(first); + queue->enqueuePacket(second); + scheduleAt(SimTime(11, SIMTIME_MS), new cMessage("extract")); + return; + } + auto gate = queueModule->gate("out"); + auto selected = par("head").boolValue() ? first : second; + CPacketFilterFunction filter([selected](const Packet *packet) { return packet == selected; }); + ASSERT(queue->canPullPacket(gate, &filter) == selected); + ASSERT(queue->getNumPackets() == 2); + ASSERT(pulls.empty() && removals == 0); + auto outsider = makePacket("outsider", false); + CPacketFilterFunction absent([outsider](const Packet *packet) { return packet == outsider; }); + ASSERT(queue->canPullPacket(gate, &absent) == nullptr); + bool rejected = false; + try { + queue->pullPacket(gate, &absent); + } + catch (const cRuntimeError&) { + rejected = true; + } + ASSERT(rejected && queue->getNumPackets() == 2); + ASSERT(pulls.empty() && removals == 0); + delete outsider; + if (par("compound").boolValue()) { + CPacketFilterFunction all(+[](const Packet *) { return true; }); + auto any = queue->canPullPacket(gate, &all); + ASSERT(any == (par("reverse").boolValue() ? second : first)); + } + auto extracted = queue->pullPacket(gate, &filter); + ASSERT(extracted == selected); + take(extracted); + ASSERT(extracted->getOwner() == this); + ASSERT(queue->getNumPackets() == 1); + ASSERT(queue->getPacket(0) == (selected == first ? second : first)); + ASSERT(removals == 0); + auto timeTag = extracted->findRegionTag(); + ASSERT(timeTag != nullptr); + ASSERT(timeTag->getPacketTotalTimes(0) == SimTime(10, SIMTIME_MS)); + auto eventTag = extracted->findRegionTag(); + ASSERT(eventTag != nullptr && eventTag->getPacketEventsArraySize() == 1); + ASSERT(eventTag->getPacketEvents(0)->getKind() == PEK_QUEUED); + ASSERT(eventTag->getPacketEvents(0)->getPacketDuration() == SimTime(10, SIMTIME_MS)); + auto retained = queue->getPacket(0); + ASSERT(retained->findRegionTag()->getPacketTotalTimes(0) == SIMTIME_ZERO); + ASSERT(retained->findRegionTag()->getPacketEventsArraySize() == 0); + if (par("compound").boolValue()) { + ASSERT(pulls.size() == 3); + auto scheduler = check_and_cast(queueModule->getSubmodule("prioritizer")); + ASSERT(scheduler->getProcessedCount() == 1); + } + else + ASSERT(pulls.size() == 1); + for (auto entry : pulls) + ASSERT(entry.second == 1); + delete extracted; + queueModule->unsubscribe(packetPulledSignal, this); + queueModule->unsubscribe(packetRemovedSignal, this); + std::cout << "Selected extraction assertions passed.\n"; + } + + void receiveSignal(cComponent *source, simsignal_t signal, cObject *, cObject *) override { + if (signal == packetPulledSignal) + pulls[source->getFullPath()]++; + else if (signal == packetRemovedSignal) + removals++; + } +}; +Define_Module(SelectiveExtractionTest); +} + +%file: test.ned +import inet.queueing.classifier.PacketClassifier; +import inet.queueing.queue.CompoundPacketQueueBase; +import inet.queueing.queue.PacketQueue; +import inet.queueing.scheduler.PriorityScheduler; +simple SelectiveScheduler extends PriorityScheduler { parameters: @class(::inet::queueing::SelectiveScheduler); } +module SelectiveCompoundQueue extends CompoundPacketQueueBase { + submodules: + classifier: PacketClassifier { + parameters: classifierClass = "inet::ieee80211::MgmtOverMulticastOverUnicastClassifier"; + } + managementQueue: PacketQueue; + multicastQueue: PacketQueue; + unicastQueue: PacketQueue; + prioritizer: SelectiveScheduler; + connections: + in --> classifier.in; + classifier.out++ --> managementQueue.in; + classifier.out++ --> multicastQueue.in; + classifier.out++ --> unicastQueue.in; + managementQueue.out --> prioritizer.in++; + multicastQueue.out --> prioritizer.in++; + unicastQueue.out --> prioritizer.in++; + prioritizer.out --> out; +} +simple SelectiveExtractionTest { + parameters: + bool compound; + bool head; + bool reverse; + @class(::inet::queueing::SelectiveExtractionTest); +} +network SelectiveExtractionNetwork { + submodules: + simpleQueue: PacketQueue; + compound: SelectiveCompoundQueue; + test: SelectiveExtractionTest; + connections allowunconnected: +} + +%inifile: omnetpp.ini +[General] +network = SelectiveExtractionNetwork +ned-path = .;../../../../src +sim-time-limit = 12ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.test.compound = ${compound=false,true,true} +*.test.reverse = ${reverse=false,false,true ! compound} +*.test.head = ${head=false,true} +*.compound.classifier.classifierClass = "inet::ieee80211::MgmtOverMulticastOverUnicastClassifier" +*.compound.prioritizer.reverseOrder = ${reverse} + +%contains: stdout +Selected extraction assertions passed. diff --git a/tests/protocol/wifi/BlockAckLifecycleTestSupport.h b/tests/protocol/wifi/BlockAckLifecycleTestSupport.h new file mode 100644 index 00000000000..4473a245d4c --- /dev/null +++ b/tests/protocol/wifi/BlockAckLifecycleTestSupport.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#ifndef __INET_PROTOCOLTEST_BLOCKACKLIFECYCLETESTSUPPORT_H +#define __INET_PROTOCOLTEST_BLOCKACKLIFECYCLETESTSUPPORT_H + +#include "WifiTestSupport.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" + +namespace inet::protocoltest { +using namespace ieee80211; + +// These queries use contracts that exist on both sides of PR #1343. +class BlockAckLifecycleHcf : public Hcf +{ + public: + bool hasOriginator(const MacAddress& peer, Tid tid) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, tid); + return agreement && agreement->getIsAddbaResponseReceived(); + } + bool hasRecipient(const MacAddress& peer, Tid tid) { + return recipientBlockAckAgreementHandler->getAgreement(tid, peer) != nullptr; + } + void sendTestBar(const MacAddress& peer, Tid tid, ITx::ICallback *callback) { + Enter_Method("sendTestBar"); + ASSERT(!tx->hasTransmission()); + auto header = makeShared(); + header->setReceiverAddress(peer); + header->setTransmitterAddress(mac->getAddress()); + header->setTidInfo(tid); + header->setStartingSequenceNumber(SequenceNumberCyclic(0)); + header->setFragmentNumber(0); + header->setDurationField(SIMTIME_ZERO); + auto frame = new Packet("test-bar", header); + frame->insertAtBack(makeShared()); + setFrameMode(frame, header, modeSet->getMode(bps(6000000))); + // Tx retains a copy. The fixture retains and deletes the original. + tx->transmitFrame(frame, header, false, callback); + delete frame; + } +}; +Define_Module(BlockAckLifecycleHcf); + +inline BlockAckLifecycleHcf *lifecycleHcf(cModule *network, const char *node) { + return check_and_cast(network->getSubmodule(node)->getSubmodule("wlan", 0) + ->getSubmodule("mac")->getSubmodule("hcf")); +} +inline Ieee80211Mib *lifecycleMib(cModule *network, const char *node) { + return check_and_cast(network->getSubmodule(node)->getSubmodule("wlan", 0)->getSubmodule("mib")); +} +} +#endif diff --git a/tests/protocol/wifi/BlockAckRequestProbe.h b/tests/protocol/wifi/BlockAckRequestProbe.h new file mode 100644 index 00000000000..182c25b523a --- /dev/null +++ b/tests/protocol/wifi/BlockAckRequestProbe.h @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#ifndef __INET_PROTOCOLTEST_BLOCKACKREQUESTPROBE_H +#define __INET_PROTOCOLTEST_BLOCKACKREQUESTPROBE_H + +#include "BlockAckLifecycleTestSupport.h" + +namespace inet::protocoltest { + +// The controlled peer uses production Tx and Rx without an originator agreement. +class BlockAckRequestProbeHcf : public BlockAckLifecycleHcf +{ + public: + simtime_t recipientDeadline(const MacAddress& peer, Tid tid) { + auto agreement = recipientBlockAckAgreementHandler->getAgreement(tid, peer); + ASSERT(agreement != nullptr); + return agreement->getExpirationTime(); + } + void sendProbeRequest(simtime_t timeout, ITx::ICallback *callback) { + Enter_Method("sendProbeRequest"); + ASSERT(!tx->hasTransmission()); + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:01")); + header->setTransmitterAddress(mac->getAddress()); + header->setTid(5); + header->setDialogToken(37); + header->setSequenceNumber(SequenceNumberCyclic(300)); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + header->setBlockAckPolicy(true); + header->setAMsduSupported(false); + header->setBufferSize(64); + header->setBlockAckTimeoutValue(timeout); + header->setDurationField(SIMTIME_ZERO); + auto frame = new Packet("probe-addba-request", header); + frame->insertAtBack(makeShared()); + setFrameMode(frame, header, modeSet->getMode(bps(6000000))); + tx->transmitFrame(frame, header, false, callback); + delete frame; + } +}; +namespace request_probe_hcf_registration { Define_Module(::inet::protocoltest::BlockAckRequestProbeHcf); } + +class BlockAckRequestProbeMac : public Ieee80211Mac, public ITx::ICallback +{ + bool responseAck = false; + public: + bool probeActive = false; + bool responseAcknowledged = false; + protected: + void handleLowerPacket(Packet *frame) override { + auto header = frame->peekAtFront(); + bool addressed = header->getReceiverAddress() == mib->address; + bool response = dynamicPtrCast(header) != nullptr; + bool delba = dynamicPtrCast(header) != nullptr; + if (probeActive && addressed && (header->getType() == ST_ACK || response || delba)) { + ASSERT(rx->lowerFrameReceived(frame)); + take(frame); + if (response || delba) { + auto ack = makeShared(); + ack->setReceiverAddress(check_and_cast(header.get())->getTransmitterAddress()); + ack->setDurationField(SIMTIME_ZERO); + auto reply = new Packet("probe-ack", ack); + reply->insertAtBack(makeShared()); + reply->addTag()->setMode(modeSet->getMode(bps(6000000))); + responseAck = response; + tx->transmitFrame(reply, ack, modeSet->getSifsTime(), false, this); + delete reply; + } + delete frame; + } + else + Ieee80211Mac::handleLowerPacket(frame); + } + void transmissionComplete(Packet *, const Ptr&) override { + if (responseAck) { + responseAcknowledged = true; + emit(registerSignal("probeResponseAcknowledged"), 1L); + } + else + emit(registerSignal("probeDelbaAcknowledged"), 1L); + } +}; +namespace request_probe_mac_registration { Define_Module(::inet::protocoltest::BlockAckRequestProbeMac); } + +class BlockAckRequestProbeStimulus : public cSimpleModule, public ITx::ICallback +{ + protected: + void initialize() override { + if (par("active")) + scheduleAt(SimTime(1), new cMessage("request")); + } + void handleMessage(cMessage *message) override { + delete message; + auto station = getParentModule()->getSubmodule("sta1")->getSubmodule("wlan", 0); + check_and_cast(station->getSubmodule("mac"))->probeActive = true; + check_and_cast(station->getSubmodule("mac")->getSubmodule("hcf")) + ->sendProbeRequest(SimTime(par("timeout").doubleValue()), this); + } + void transmissionComplete(Packet *, const Ptr&) override {} +}; +namespace request_probe_stimulus_registration { Define_Module(::inet::protocoltest::BlockAckRequestProbeStimulus); } +} +#endif diff --git a/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test b/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test new file mode 100644 index 00000000000..4f9cb137cdd --- /dev/null +++ b/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test @@ -0,0 +1,134 @@ +%description: +A crafted Basic BAR crosses the production transmitter and wireless receive path. +Without an agreement, the AP must send recipient-role DELBA for the selected peer and TID. +The control establishes an agreement through ADDBA and requires a Basic Block Ack response. +These checks cover INET Basic compatibility behavior, not full standards conformance. + +%extraargs: -r 0..1 + +%file: BarWithoutAgreement.cc +#include "BlockAckLifecycleTestSupport.h" + +namespace inet::protocoltest { +class LifecycleBarMac : public Ieee80211Mac +{ + public: + bool testBarOutstanding = false; + protected: + void handleLowerPacket(Packet *frame) override { + auto ba = dynamicPtrCast(frame->peekAtFront()); + if (testBarOutstanding && ba && ba->getReceiverAddress() == mib->address) { + ASSERT(rx->lowerFrameReceived(frame)); + take(frame); + ASSERT(ba->getTidInfo() == 6); + testBarOutstanding = false; + emit(registerSignal("testBarResponseReceived"), 1L); + delete frame; + } + else + Ieee80211Mac::handleLowerPacket(frame); + } +}; +Define_Module(LifecycleBarMac); + +class LifecycleBarStimulus : public cSimpleModule, public ITx::ICallback +{ + protected: + void initialize() override { scheduleAt(SimTime(1100, SIMTIME_MS), new cMessage("bar")); } + void handleMessage(cMessage *message) override { + delete message; + auto network = getParentModule(); + bool control = par("control"); + Tid tid = control ? 6 : 5; + auto sender = lifecycleHcf(network, "sta1"); + auto recipient = lifecycleHcf(network, "ap"); + auto peer = lifecycleMib(network, "ap")->address; + auto station = lifecycleMib(network, "sta1")->address; + ASSERT(sender->hasOriginator(peer, tid) == control); + ASSERT(recipient->hasRecipient(station, tid) == control); + if (control) + check_and_cast(network->getSubmodule("sta1")->getSubmodule("wlan", 0)->getSubmodule("mac"))->testBarOutstanding = true; + sender->sendTestBar(peer, tid, this); + } + void transmissionComplete(Packet *, const Ptr&) override {} +}; +Define_Module(LifecycleBarStimulus); + +Define_ProtocolTest(wifi_block_ack_orphan_bar) +{ + return ProtocolTest("wifi_block_ack_orphan_bar") + .once(on("ap.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + auto bar = dynamicPtrCast(c.event.packet->peekAtFront()); + return bar && bar->getTidInfo() == 5 && bar->getTransmitterAddress() == MacAddress("02:00:00:00:00:03"); + }).describe("orphan BAR received through the radio").within(1.2)) + .once(on("ap.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).assertEvent([](const MatchContext& c) { + auto delba = c.event.packet->peekAtFront(); + return delba->getReceiverAddress() == MacAddress("02:00:00:00:00:03") && + delba->getTid() == 5 && !delba->getInitiator() && + !lifecycleHcf(c.event.node->getParentModule(), "ap")->hasRecipient(MacAddress("02:00:00:00:00:03"), 5); + }).describe("recipient-role DELBA without agreement allocation").within(0.02)); +} + +Define_ProtocolTest(wifi_block_ack_known_bar) +{ + return ProtocolTest("wifi_block_ack_known_bar") + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { return std::string(c.event.packet->getName()) == "test-bar"; }) + .describe("control BAR after real ADDBA setup").within(1.2)) + .meanwhile(never(on("ap.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).describe("no DELBA for the established agreement").within(0.02))) + .once(on("ap.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).assertEvent([](const MatchContext& c) { + auto ba = c.event.packet->peekAtFront(); + return ba->getReceiverAddress() == MacAddress("02:00:00:00:00:03") && ba->getTidInfo() == 6; + }).describe("Basic Block Ack for an established agreement").within(0.02)) + .once(on("sta1.wlan[0].mac").signal("testBarResponseReceived").assertValue(1) + .describe("test peer receives the Basic Block Ack through the radio").within(0.01)); +} +} + +%file: BarWithoutAgreement.ned +@namespace(inet::protocoltest); +import inet.protocoltest.wifi.WifiInfraNetwork; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +module LifecycleBarMac extends Ieee80211Mac { + parameters: + @class(LifecycleBarMac); + @signal[testBarResponseReceived](type=long); +} +simple LifecycleBarStimulus { + parameters: + bool control; + @class(LifecycleBarStimulus); +} +network LifecycleBarNetwork extends WifiInfraNetwork { + submodules: + stimulus: LifecycleBarStimulus; +} + +%inifile: test.ini +[General] +network = LifecycleBarNetwork +*.sta1.wlan[*].mac.typename = "LifecycleBarMac" +*.stimulus.control = ${control=false,true} +*.tester.testName = ${control} ? "wifi_block_ack_known_bar" : "wifi_block_ack_orphan_bar" +*.sta1.app[0].startTime = ${control} ? 1s : 2s +*.sta1.app[0].stopTime = ${control} ? 1.003s : 2.003s +*.sta1.app[0].sendInterval = 0.1ms +sim-time-limit = 1.2s +include ../../ini/_blockackLifecycle.ini + +%contains: stdout +PROTOCOLTEST wifi_block_ack_orphan_bar: PASS +%contains: stdout +PROTOCOLTEST wifi_block_ack_known_bar: PASS +%not-contains: stdout +: FAIL diff --git a/tests/protocol/wifi/common/WifiBlockAckDialogToken.test b/tests/protocol/wifi/common/WifiBlockAckDialogToken.test new file mode 100644 index 00000000000..63c8ad3a97a --- /dev/null +++ b/tests/protocol/wifi/common/WifiBlockAckDialogToken.test @@ -0,0 +1,137 @@ +%description: +Run 0 checks token echo for a crafted ADDBA Request with token 37. +Run 1 changes the response token before transmission during real production setup. +The originator must reject that response and defer Block Ack-policy DATA. +Run 2 retains the response token and requires accepted setup and Block Ack-policy DATA. +All frames cross the radio. Read-only observations check production agreement state. + +%extraargs: -r 0..2 + +%file: DialogToken.cc +#include "BlockAckRequestProbe.h" + +namespace inet::protocoltest { +class DialogTokenHcf : public BlockAckRequestProbeHcf +{ + protected: + void processMgmtFrame(Packet *frame, const Ptr& header) override { + if (par("wrongToken") && dynamicPtrCast(header)) { + auto response = frame->removeAtFront(); + response->setDialogToken(response->getDialogToken() == 255 ? 1 : response->getDialogToken() + 1); + frame->insertAtFront(response); + std::cout << "Response token changed at " << simTime() << ": " << int(response->getDialogToken()) << ".\n"; + emit(registerSignal("responseTokenChanged"), 1L); + Hcf::processMgmtFrame(frame, response); + } + else + Hcf::processMgmtFrame(frame, header); + } + void processLowerFrame(Packet *frame, const Ptr& header) override { + Enter_Method("processLowerFrame"); + auto response = dynamicPtrCast(header); + Hcf::processLowerFrame(frame, header); + if (response) { + bool accepted = hasOriginator(response->getTransmitterAddress(), response->getTid()); + std::cout << "Response processed at " << simTime() << ": token=" << int(response->getDialogToken()) + << ", originator accepted=" << accepted << ".\n"; + emit(registerSignal("originatorAccepted"), long(accepted)); + } + } +}; +namespace dialog_token_registration { Define_Module(::inet::protocoltest::DialogTokenHcf); } + +Define_ProtocolTest(wifi_block_ack_token_echo) +{ + return ProtocolTest("wifi_block_ack_token_echo") + .once(on("ap.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).assertEvent([](const MatchContext& c) { + auto request = c.event.packet->peekAtFront(); + return request->getDialogToken() == 37 && request->getTid() == 5; + }).describe("recipient receives request token 37").within(1.1)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).assertEvent([](const MatchContext& c) { + auto response = c.event.packet->peekAtFront(); + std::cout << "Echo response token at " << simTime() << ": " << int(response->getDialogToken()) << ".\n"; + return response->getDialogToken() == 37 && response->getTid() == 5 && + response->getStatusCode() == SC_SUCCESSFUL && response->getTransmitterAddress() == MacAddress("02:00:00:00:00:01"); + }).describe("response echoes request token 37").within(0.01)) + .once(on("sta1.wlan[0].mac").signal("probeResponseAcknowledged").assertValue(1) + .describe("controlled peer acknowledges the actual response").within(0.01)); +} + +ProtocolTest tokenCorrelationTest(const char *name, bool accepted) +{ + auto test = ProtocolTest(name); + test.once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).capture("requestToken", [](const PacketEvent& event) { + return cValue(event.packet->peekAtFront()->getDialogToken()); + }).describe("production request identifies its token").within(1.1)); + if (!accepted) + test.once(on("ap.wlan[0].mac.hcf").signal("responseTokenChanged").assertValue(1) + .describe("one response token changes before transmission").within(0.01)); + test.once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).assertEvent([accepted](const MatchContext& c) { + auto response = c.event.packet->peekAtFront(); + return (response->getDialogToken() == c.get("requestToken").intValue()) == accepted && + response->getTid() == 6 && response->getStatusCode() == SC_SUCCESSFUL; + }).describe("actual response has the selected token relationship").within(0.01)); + if (!accepted) + test.meanwhile(never(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + auto data = dynamicPtrCast(c.event.packet->peekAtFront()); + return data && data->getTid() == 6 && data->getAckPolicy() == BLOCK_ACK; + }).describe("wrong token cannot release Block Ack-policy DATA").within(0.005))); + test.once(on("sta1.wlan[0].mac.hcf").signal("originatorAccepted").assertValue(accepted ? 1 : 0) + .describe("originator applies response correlation").within(0.001)); + if (accepted) + test.once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + auto data = dynamicPtrCast(c.event.packet->peekAtFront()); + return data && data->getTid() == 6 && data->getAckPolicy() == BLOCK_ACK; + }).describe("matching response permits Block Ack-policy DATA").within(0.01)); + return test; +} +Define_ProtocolTest(wifi_block_ack_wrong_token) { return tokenCorrelationTest("wifi_block_ack_wrong_token", false); } +Define_ProtocolTest(wifi_block_ack_matching_token) { return tokenCorrelationTest("wifi_block_ack_matching_token", true); } +} + +%file: DialogToken.ned +@namespace(inet::protocoltest); +import inet.protocoltest.wifi.BlockAckRequestProbeHcf; +module DialogTokenHcf extends BlockAckRequestProbeHcf { + parameters: + bool wrongToken = default(false); + @class(DialogTokenHcf); + @signal[responseTokenChanged](type=long); + @signal[originatorAccepted](type=long); +} + +%inifile: test.ini +[General] +network = inet.protocoltest.wifi.BlockAckRequestProbeNetwork +*.stimulus.active = ${scenario=0,1,2} == 0 +*.tester.testName = ${scenario} == 0 ? "wifi_block_ack_token_echo" : (${scenario} == 1 ? "wifi_block_ack_wrong_token" : "wifi_block_ack_matching_token") +*.sta1.wlan[*].mac.typename = "inet.protocoltest.wifi.BlockAckRequestProbeMac" +*.sta1.wlan[*].mac.hcf.typename = "DialogTokenHcf" +*.ap.wlan[*].mac.hcf.typename = "DialogTokenHcf" +*.ap.wlan[*].mac.hcf.wrongToken = ${scenario} == 1 +*.sta1.app[0].startTime = ${scenario} == 0 ? 2s : 1s +*.sta1.app[0].stopTime = ${scenario} == 0 ? 2.003s : 1.003s +*.sta1.app[0].sendInterval = 0.1ms +sim-time-limit = 1.1s +include ../../ini/_blockackLifecycle.ini + +%contains: stdout +PROTOCOLTEST wifi_block_ack_token_echo: PASS +%contains: stdout +PROTOCOLTEST wifi_block_ack_wrong_token: PASS +%contains: stdout +PROTOCOLTEST wifi_block_ack_matching_token: PASS diff --git a/tests/protocol/wifi/common/WifiBlockAckDuplicateAddba.test b/tests/protocol/wifi/common/WifiBlockAckDuplicateAddba.test new file mode 100644 index 00000000000..288f8356146 --- /dev/null +++ b/tests/protocol/wifi/common/WifiBlockAckDuplicateAddba.test @@ -0,0 +1,174 @@ +%description: +Production ADDBA setup encounters one lost request ACK and a held response queue. +The recipient must acknowledge the request retry without a second agreement dispatch. +The response hold ends after retry reception. Setup and Block Ack-policy DATA must follow. +The dispatch observer calls the production handler unchanged. All frames cross the radio. + +%file: DuplicateAddba.cc +#include +#include "BlockAckLifecycleTestSupport.h" + +namespace inet::protocoltest { +class DuplicateAddbaMac : public Ieee80211Mac +{ + bool requestSent = false; + public: + int suppressed = 0; + protected: + void sendDownFrame(Packet *frame) override { + if (dynamicPtrCast(frame->peekAtFront())) + requestSent = true; + Ieee80211Mac::sendDownFrame(frame); + } + void handleLowerPacket(Packet *frame) override { + auto header = frame->peekAtFront(); + if (requestSent && suppressed == 0 && header->getType() == ST_ACK && header->getReceiverAddress() == mib->address) { + suppressed++; + emit(registerSignal("requestAckSuppressed"), 1L); + delete frame; + } + else + Ieee80211Mac::handleLowerPacket(frame); + } +}; +Define_Module(DuplicateAddbaMac); + +class DuplicateAddbaHcf : public BlockAckLifecycleHcf +{ + Ptr original; + int received = 0; + int transmittedRequestAcks = 0; + std::set requestAckTreeIds; + cMessage *release = nullptr; + public: + int dispatches = 0; + int requestAcks = 0; + bool responseHeld = false; + ~DuplicateAddbaHcf() { cancelAndDelete(release); } + protected: + void processMgmtFrame(Packet *frame, const Ptr& header) override { + if (dynamicPtrCast(header) && !responseHeld) { + responseHeld = true; + rx->setContentionBlocked(true); + } + Hcf::processMgmtFrame(frame, header); + } + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + if (dynamicPtrCast(header)) + dispatches++; + Hcf::recipientProcessReceivedManagementFrame(header); + } + void transmitControlResponseFrame(Packet *response, const Ptr& responseHeader, + Packet *receivedFrame, const Ptr& receivedHeader) override { + if (dynamicPtrCast(receivedHeader) && responseHeader->getType() == ST_ACK) { + requestAcks++; + requestAckTreeIds.insert(response->getTreeId()); + } + Hcf::transmitControlResponseFrame(response, responseHeader, receivedFrame, receivedHeader); + } + void processLowerFrame(Packet *frame, const Ptr& header) override { + Enter_Method("processLowerFrame"); + auto request = dynamicPtrCast(header); + Hcf::processLowerFrame(frame, header); + if (request) { + received++; + if (received == 1) { + ASSERT(!request->getRetry()); + original = request; + } + else { + ASSERT(request->getRetry()); + ASSERT(request->getTransmitterAddress() == original->getTransmitterAddress()); + ASSERT(request->getReceiverAddress() == original->getReceiverAddress()); + ASSERT(request->getTid() == original->getTid()); + ASSERT(request->getDialogToken() == original->getDialogToken()); + ASSERT(request->getSequenceNumber() == original->getSequenceNumber()); + ASSERT(request->getFragmentNumber() == original->getFragmentNumber()); + ASSERT(release == nullptr); + release = new cMessage("release-response"); + scheduleAfter(SimTime(1, SIMTIME_MS), release); + } + std::cout << "ADDBA reception at " << simTime() << ": received=" << received + << ", dispatches=" << dispatches << ", ACK submissions=" << requestAcks << ".\n"; + emit(registerSignal("addbaReception"), long(received)); + } + } + void recipientProcessTransmittedControlResponseFrame(Packet *frame, const Ptr& header) override { + if (header->getType() == ST_ACK && requestAckTreeIds.erase(frame->getTreeId()) != 0) + transmittedRequestAcks++; + Hcf::recipientProcessTransmittedControlResponseFrame(frame, header); + } + void handleMessage(cMessage *message) override { + if (message == release) { + delete release; + release = nullptr; + rx->setContentionBlocked(false); + emit(registerSignal("requestAcksTransmitted"), long(transmittedRequestAcks)); + emit(registerSignal("responseReleased"), 1L); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(DuplicateAddbaHcf); + +Define_ProtocolTest(wifi_block_ack_duplicate_addba) +{ + return ProtocolTest("wifi_block_ack_duplicate_addba") + .once(on("ap.wlan[0].mac.hcf").signal("addbaReception").filterValue(1) + .describe("original ADDBA Request received").within(1.2)) + .once(on("sta1.wlan[0].mac").signal("requestAckSuppressed").filterValue(1) + .describe("request ACK deliberately lost").within(0.01)) + .once(on("ap.wlan[0].mac.hcf").signal("addbaReception").filterValue(2) + .assertEvent([](const MatchContext& c) { + auto hcf = check_and_cast(c.event.module); + return hcf->dispatches == 1 && hcf->requestAcks == 2 && hcf->responseHeld; + }).describe("retry acknowledged without duplicate agreement dispatch").within(0.01)) + .once(on("ap.wlan[0].mac.hcf").signal("requestAcksTransmitted").assertValue(2) + .describe("both request ACKs completed physical transmission").within(0.01)) + .once(on("ap.wlan[0].mac.hcf").signal("responseReleased").filterValue(1) + .describe("actual response queue released").within(0.01)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).describe("ADDBA Response received through the radio").within(0.01)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + auto data = dynamicPtrCast(c.event.packet->peekAtFront()); + return data && data->getAckPolicy() == BLOCK_ACK; + }).assertEvent([](const MatchContext& c) { + auto network = c.event.node->getParentModule(); + return lifecycleHcf(network, "sta1")->hasOriginator(MacAddress("02:00:00:00:00:01"), 6); + }).describe("DATA uses the accepted agreement").within(0.02)); +} +} + +%file: DuplicateAddba.ned +@namespace(inet::protocoltest); +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.protocoltest.wifi.BlockAckLifecycleHcf; +module DuplicateAddbaMac extends Ieee80211Mac { + parameters: + @class(DuplicateAddbaMac); + @signal[requestAckSuppressed](type=long); +} +module DuplicateAddbaHcf extends BlockAckLifecycleHcf { + parameters: + @class(DuplicateAddbaHcf); + @signal[addbaReception](type=long); + @signal[responseReleased](type=long); + @signal[requestAcksTransmitted](type=long); +} + +%inifile: test.ini +[General] +*.tester.testName = "wifi_block_ack_duplicate_addba" +*.sta1.wlan[*].mac.typename = "DuplicateAddbaMac" +*.ap.wlan[*].mac.hcf.typename = "DuplicateAddbaHcf" +*.sta1.app[0].sendInterval = 0.1ms +*.sta1.app[0].stopTime = 1.02s +sim-time-limit = 1.1s +include ../../ini/_blockackLifecycle.ini + +%contains: stdout +PROTOCOLTEST wifi_block_ack_duplicate_addba: PASS diff --git a/tests/protocol/wifi/common/WifiBlockAckInactivity.test b/tests/protocol/wifi/common/WifiBlockAckInactivity.test new file mode 100644 index 00000000000..500cc87a51c --- /dev/null +++ b/tests/protocol/wifi/common/WifiBlockAckInactivity.test @@ -0,0 +1,133 @@ +%description: +A controlled peer establishes a recipient agreement through a real ADDBA exchange at 1 s. +The finite-timeout run observes agreement state before, at, and after its stored absolute deadline. +The recipient must retire the agreement at that deadline and send recipient-role DELBA. +The zero-timeout control requires the agreement to remain active without DELBA. +The fixture reads agreement state; it never changes a deadline or schedules the production timer. + +%extraargs: -r 0..1 + +%file: Inactivity.cc +#include "BlockAckRequestProbe.h" + +namespace inet::protocoltest { +class BlockAckInactivityObserver : public cSimpleModule +{ + public: + BlockAckInactivityObserver() : cSimpleModule(65536) {} + protected: + void activity() override { + wait(SimTime(1001, SIMTIME_MS)); + auto network = getParentModule(); + auto peer = MacAddress("02:00:00:00:00:03"); + auto recipient = check_and_cast(lifecycleHcf(network, "ap")); + auto stationMac = check_and_cast(network->getSubmodule("sta1")->getSubmodule("wlan", 0)->getSubmodule("mac")); + ASSERT(stationMac->responseAcknowledged); + ASSERT(recipient->hasRecipient(peer, 5)); + auto deadline = recipient->recipientDeadline(peer, 5); + bool disabled = par("disabled"); + if (disabled) { + ASSERT(deadline == SIMTIME_MAX); + emit(registerSignal("disabledAgreementActive"), 1L); + wait(SimTime(24, SIMTIME_MS)); + std::cout << "Zero-timeout agreement at " << simTime() << ": active=" << recipient->hasRecipient(peer, 5) << ".\n"; + emit(registerSignal("disabledAgreementRetained"), long(recipient->hasRecipient(peer, 5))); + } + else { + ASSERT(deadline > simTime() && deadline < SimTime(1030, SIMTIME_MS)); + std::cout << "Recipient absolute deadline: " << deadline << ".\n"; + emit(registerSignal("recipientDeadline"), deadline.dbl()); + wait(deadline - simTime() - SimTime(1, SIMTIME_NS)); + emit(registerSignal("agreementBeforeDeadline"), long(recipient->hasRecipient(peer, 5))); + wait(SimTime(1, SIMTIME_NS)); + std::cout << "Agreement at absolute deadline " << simTime() << ": active=" << recipient->hasRecipient(peer, 5) << ".\n"; + emit(registerSignal("agreementAtDeadline"), long(recipient->hasRecipient(peer, 5))); + wait(SimTime(1, SIMTIME_NS)); + emit(registerSignal("agreementAfterDeadline"), long(recipient->hasRecipient(peer, 5))); + } + } +}; +namespace inactivity_observer_registration { Define_Module(::inet::protocoltest::BlockAckInactivityObserver); } + +Define_ProtocolTest(wifi_block_ack_inactivity) +{ + return ProtocolTest("wifi_block_ack_inactivity") + .once(on("sta1.wlan[0].mac").signal("probeResponseAcknowledged").assertValue(1) + .describe("actual ADDBA Response receives an ACK").within(1.01)) + .once(on("observer").signal("recipientDeadline") + .assertEvent([](const MatchContext& c) { return c.event.value > 1 && c.event.value < 1.03; }) + .capture("absoluteDeadline", [](const PacketEvent& event) { return cValue(event.value); }) + .describe("recipient reports a finite absolute deadline after 1 s").within(0.001)) + .meanwhile(atLeastTimes(1, on("ap.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).assertEvent([](const MatchContext& c) { + auto delba = c.event.packet->peekAtFront(); + return delba->getReceiverAddress() == MacAddress("02:00:00:00:00:03") && + delba->getTid() == 5 && !delba->getInitiator() && delba->getReasonCode() == 39 && + c.event.time >= SimTime(c.get("absoluteDeadline").doubleValue()); + }).describe("recipient transmits timeout DELBA at or after expiry").within(0.03))) + .once(on("observer").signal("agreementBeforeDeadline").assertValue(1) + .describe("agreement remains active one nanosecond before expiry").within(0.03)) + .once(on("observer").signal("agreementAtDeadline").assertValue(0) + .describe("agreement is absent at the absolute deadline").within(0.000001)) + .once(on("observer").signal("agreementAfterDeadline").assertValue(0) + .describe("agreement remains absent after the deadline").within(0.000001)) + .once(on("sta1.wlan[0].mac").signal("probeDelbaAcknowledged").assertValue(1) + .describe("controlled peer acknowledges timeout DELBA").within(0.005)); +} + +Define_ProtocolTest(wifi_block_ack_inactivity_disabled) +{ + return ProtocolTest("wifi_block_ack_inactivity_disabled") + .once(on("sta1.wlan[0].mac").signal("probeResponseAcknowledged").assertValue(1) + .describe("zero-timeout agreement completes ADDBA").within(1.01)) + .once(on("observer").signal("disabledAgreementActive").assertValue(1) + .describe("zero timeout supplies no finite deadline").within(0.001)) + .meanwhile(never(on("ap.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { + return dynamicPtrCast(c.event.packet->peekAtFront()) != nullptr; + }).describe("no timeout DELBA during the control window").within(0.03))) + .once(on("observer").signal("disabledAgreementRetained").assertValue(1) + .describe("zero-timeout agreement remains active").within(0.03)); +} +} + +%file: Inactivity.ned +@namespace(inet::protocoltest); +import inet.protocoltest.wifi.BlockAckRequestProbeNetwork; +simple BlockAckInactivityObserver { + parameters: + bool disabled; + @class(BlockAckInactivityObserver); + @signal[recipientDeadline](type=double); + @signal[agreementBeforeDeadline](type=long); + @signal[agreementAtDeadline](type=long); + @signal[agreementAfterDeadline](type=long); + @signal[disabledAgreementActive](type=long); + @signal[disabledAgreementRetained](type=long); +} +network BlockAckInactivityNetwork extends BlockAckRequestProbeNetwork { + submodules: + observer: BlockAckInactivityObserver; +} + +%inifile: test.ini +[General] +network = BlockAckInactivityNetwork +*.observer.disabled = ${disabled=false,true} +*.stimulus.timeout = ${disabled} ? 0s : 20ms +*.tester.testName = ${disabled} ? "wifi_block_ack_inactivity_disabled" : "wifi_block_ack_inactivity" +*.sta1.wlan[*].mac.typename = "inet.protocoltest.wifi.BlockAckRequestProbeMac" +*.sta1.wlan[*].mac.hcf.typename = "inet.protocoltest.wifi.BlockAckRequestProbeHcf" +*.ap.wlan[*].mac.hcf.typename = "inet.protocoltest.wifi.BlockAckRequestProbeHcf" +*.ap.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.blockAckTimeoutValue = ${disabled} ? 0s : 20ms +*.sta1.app[0].startTime = 2s +*.sta1.app[0].stopTime = 2.003s +sim-time-limit = 1.1s +include ../../ini/_blockackLifecycle.ini + +%contains: stdout +PROTOCOLTEST wifi_block_ack_inactivity: PASS +%contains: stdout +PROTOCOLTEST wifi_block_ack_inactivity_disabled: PASS diff --git a/tests/protocol/wifi/ini/_blockackLifecycle.ini b/tests/protocol/wifi/ini/_blockackLifecycle.ini new file mode 100644 index 00000000000..26bf3f58690 --- /dev/null +++ b/tests/protocol/wifi/ini/_blockackLifecycle.ini @@ -0,0 +1,26 @@ +# Basic compatibility tests use real association and production MAC exchanges. +[General] +cmdenv-express-mode = true +cmdenv-event-banners = false +**.cmdenv-log-level = warn +**.wlan[*].mac.hcf.typename = "inet.protocoltest.wifi.BlockAckLifecycleHcf" +**.wlan[*].classifier.udpPortUpMap = "5000 VO 5001 VO" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.blockAckTimeoutValue = 0s +**.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.blockAckTimeoutValue = 0s +**.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 10000B +**.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 100 +**.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 100000 +**.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.hcf.originatorAckPolicy.blockAckReqThreshold = 5 +*.ap.wlan[*].address = "02:00:00:00:00:01" +*.sta1.wlan[*].address = "02:00:00:00:00:03" +*.sta2.wlan[*].address = "02:00:00:00:00:04" +*.ap.wlan[*].mgmt.ssid = "lifecycle" +*.sta*.wlan[*].agent.defaultSsid = "lifecycle" +*.sta*.wlan[*].agent.channelsToScan = "0" +*.sta1.wlan[*].agent.startingTime = 0s +*.sta2.wlan[*].agent.startingTime = 0.2s +include _n.ini +include _base.ini diff --git a/tests/protocol/wifi/ned/BlockAckLifecycleHcf.ned b/tests/protocol/wifi/ned/BlockAckLifecycleHcf.ned new file mode 100644 index 00000000000..de2358750ea --- /dev/null +++ b/tests/protocol/wifi/ned/BlockAckLifecycleHcf.ned @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +package inet.protocoltest.wifi; + +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; + +module BlockAckLifecycleHcf extends Hcf +{ + parameters: + @class(::inet::protocoltest::BlockAckLifecycleHcf); +} diff --git a/tests/protocol/wifi/ned/BlockAckRequestProbe.ned b/tests/protocol/wifi/ned/BlockAckRequestProbe.ned new file mode 100644 index 00000000000..e9fe6930dfd --- /dev/null +++ b/tests/protocol/wifi/ned/BlockAckRequestProbe.ned @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +package inet.protocoltest.wifi; + +import inet.linklayer.ieee80211.mac.Ieee80211Mac; + +module BlockAckRequestProbeHcf extends BlockAckLifecycleHcf { + parameters: + @class(::inet::protocoltest::BlockAckRequestProbeHcf); +} +module BlockAckRequestProbeMac extends Ieee80211Mac { + parameters: + @class(::inet::protocoltest::BlockAckRequestProbeMac); + @signal[probeResponseAcknowledged](type=long); + @signal[probeDelbaAcknowledged](type=long); +} +simple BlockAckRequestProbeStimulus { + parameters: + bool active = default(true); + double timeout @unit(s) = default(0s); + @class(::inet::protocoltest::BlockAckRequestProbeStimulus); +} +network BlockAckRequestProbeNetwork extends WifiInfraNetwork { + submodules: + stimulus: BlockAckRequestProbeStimulus; +}